diff --git a/.gitignore b/.gitignore index 940794e..6160e9e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.user *.userosscache *.sln.docstates +*.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/README.md b/README.md index ef9c25e..52bddf5 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,100 @@ # Overview -A relay utility for bots based on Azure Service Bus. -This utility allows you to forward a message sent to a bot hosted on any channel to your local machine. +**NetPassage** allows you to expose a web service, such as Microsoft Bot running on your local machine or on the private network to the public cloud endpoint, such as Bot Channel Registration Messaging endpoint, for example, via Azure Service Bus Relay. + +This client side utility supports both `Http` and `WebSocket` connectivity with the cloud based Relay. 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 -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). +When you start `NetPassage`, it will display a UI in your terminal with the public URL of your tunnel and other status and metrics information about connections made over your tunnel. -# How to configure and run the utility -### Building with .Net Framework +![UI Terminal](docs/images/WebSocketConsole.png) -1. Once the solution has been cloned to your machine, open the solution in Visual Studio. +## Architecture -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. +`NetPassage`uses Microsoft Azure Service Bus Relay to tunnel all incoming +messages thru the Relay's hybrid connections (either Websocket or Http) and to +the remotely running (e.g. local) `NetPassage`client utility's listener, as +shown in the architecture diagram below: - - 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**. +![Architecture](docs/images/passage.png) -8. Once testing is completed, you can compile the project into an executable. +## How to configure and run the utility - a. Right click the project folder in Visual Studio and select **Build**. +The `NetPassage` utility is constructed from the following parts: - 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. +1. NetPassage client console app +2. Microsoft.HybridConnectionsRelay a server side job (deployed to Azure) +3. Microsoft.HybridConnections.Core -### Building with .Net Core +### Building with Microsoft Visual Studio 2019 + +>Note: If you plan on using only Http tunnel protocol, then you would only need to build the NetPassage and Microsoft.HybridConnections.Core projects. Then you would start NetPassage project only. 1. Once the solution has been cloned to your machine, open the solution in Visual Studio. -2. In Solution Explorer, expand the **ServiceBusRelayUtilNetCore** folder. - -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]**. - - b. "RelayName" is the name of the hybrid connection created in step 12. Enter the value in place of **[Your Relay Name]**. - - 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]**. - - 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. +2. In Solution Explorer, expand the **NetPassage** folder. - - 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**. +3. Clone the **NetPassage.json.template** file into **NetPassage.json** and replace the following values with those from your Azure Service +Bus. + + a. `Namespace` is the name of your Azure Service Bus Relay. Enter the same value for both Http and Websocket sections. + + b. Under **Http** section, `ConnectionName` is the name of the Hybrid Connection used for Http relay. And Under **Websocket** section, `ConnectionName` is the name of the Hybrid Connection used for Websocket relay. + + c. "PolicyName" is the value to the shared access policy for each of the Hybrid Connections you've entered earlier. + + d. "PolicyKey" is the secret key value for the shared access policy. + + 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. For example, `http:/localhost:[PORT]`. + +If you're going to use the `Websocket` relay, you'd also need to update the values in the **appsettings.json** for the `Microsoft.HybridConnections.Relay` project. + +1. In Solution Explorer, expand the **Microsoft.HybridConnections.Relay** folder. + +2. Clone the **appsettings.json.template** file into **appsettings.json** and replace the following values with those from your Azure Service +Bus. -8. Once testing is completed, you can compile the project into an executable. + a. `Namespace` is the name of your Azure Service Bus Relay. Enter the same value for both Http and Websocket sections. - a. Right click the project folder in Visual Studio and select **Publish**. + b. Under **Listener** section, `ConnectionName` is the name of the Hybrid Connection used for Http relay. And Under **Relay** section, `ConnectionName` is the name of the Hybrid Connection used for Websocket relay. - b. For **Pick a publish Target**, select **Folder**. + c. "PolicyName" is the value to the shared access policy for each of the Hybrid Connections you've entered earlier. - c. For **Folder or File Share**, choose an output location or keep the default. + d. "PolicyKey" is the secret key value for the shared access policy. - d. Click **Create Profile** to create a publish profile. + 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. For example, `http:/localhost:[PORT]`. - e. Click **Configure...** to change the build configuration and change the following: +Before testing the relay, your Azure Web Bot's messaging endpoint must be updated to match the relay. - - **Configuration** to "Debug | Any CPU" - - **Deployment Mode** to "Self-contained" - - **Target Runtime** to "win-x64" +1. Login to the Azure portal and open your Web App Bot. + +2. Select **Settings** under Bot management to open the settings blade. + +3. In the **Messaging endpoint** field, enter the service bus namespace and relay. The relay should match the relay `ConnectionName` entered in the **NetPassage.json** file and should not exist in Azure. + +4. Append **"/api/messages"** to the end to create the full endpoint to be used. For example, `https://example-service-bus.servicebus.windows.net/websocketrelay/api/messages`. + +5. Click **Save** when completed. + +Now, back to the Visual Studio. + +1. In Visual Studio, if you want to run in `Websocket` mode, make sure both `NetPassage` and `Microsoft.HybridConnections.Relay` projects are selected to start. Then, press **F5** to run both projects. +And, if you're planning on using only `Http` mode, you should only run `NetPassage` project. + +2. Open and run your locally hosted bot. + +3. 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**. + +4. 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. - f. Click **Save** and then **Publish** +## Acknowledgments - 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. +Part of this code is based on the work that [Gabo Gilabert](https://github.com/gabog) did in his project [here](https://github.com/gabog/AzureServiceBusBotRelay). 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/App.ico b/Src/ServiceBusRelayUtil/App.ico deleted file mode 100644 index f3c2e20..0000000 Binary files a/Src/ServiceBusRelayUtil/App.ico and /dev/null differ diff --git a/Src/ServiceBusRelayUtil/DispatcherService.cs b/Src/ServiceBusRelayUtil/DispatcherService.cs deleted file mode 100644 index 13f81ea..0000000 --- a/Src/ServiceBusRelayUtil/DispatcherService.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.ServiceModel; -using System.ServiceModel.Channels; -using System.ServiceModel.Web; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using Microsoft.ServiceBus.Web; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Formatting = Newtonsoft.Json.Formatting; - -namespace GaboG.ServiceBusRelayUtil -{ - [ServiceContract(Namespace = "http://samples.microsoft.com/ServiceModel/Relay/")] - [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)] - internal class DispatcherService - { - private static readonly HashSet _httpContentHeaders = new HashSet - { - "Allow", - "Content-Encoding", - "Content-Language", - "Content-Length", - "Content-Location", - "Content-MD5", - "Content-Range", - "Content-Type", - "Expires", - "Last-Modified" - }; - - private readonly ServiceBusRelayUtilConfig _config; - - public DispatcherService(ServiceBusRelayUtilConfig config) - { - _config = config; - } - - [WebGet(UriTemplate = "*")] - [OperationContract(AsyncPattern = true)] - public async Task GetAsync() - { - try - { - var ti0 = DateTime.Now; - Console.WriteLine("In GetAsync:"); - var context = WebOperationContext.Current; - var request = BuildForwardedRequest(context, null); - Console.WriteLine("...calling {0}...", request.RequestUri); - HttpResponseMessage response; - using (var client = new HttpClient()) - { - response = await client.SendAsync(request, CancellationToken.None); - } - - Console.WriteLine("...and back {0:N0} ms...", DateTime.Now.Subtract(ti0).TotalMilliseconds); - Console.WriteLine(""); - - Console.WriteLine("...reading and creating response..."); - CopyHttpResponseMessageToOutgoingResponse(response, context.OutgoingResponse); - var stream = response.Content != null ? await response.Content.ReadAsStreamAsync() : null; - var message = StreamMessageHelper.CreateMessage(MessageVersion.None, "GETRESPONSE", stream ?? new MemoryStream()); - Console.WriteLine("...and done (total time: {0:N0} ms).", DateTime.Now.Subtract(ti0).TotalMilliseconds); - Console.WriteLine(""); - return message; - } - catch (Exception ex) - { - WriteException(ex); - throw; - } - } - - [WebInvoke(UriTemplate = "*", Method = "*")] - [OperationContract(AsyncPattern = true)] - public async Task InvokeAsync(Message msg) - { - try - { - var ti0 = DateTime.Now; - WriteFlowerLine(); - Console.WriteLine("In InvokeAsync:"); - var context = WebOperationContext.Current; - var request = BuildForwardedRequest(context, msg); - Console.WriteLine("...calling {0}", request.RequestUri); - HttpResponseMessage response; - using (var client = new HttpClient()) - { - response = await client.SendAsync(request, CancellationToken.None); - } - - Console.WriteLine("...and done {0:N0} ms...", DateTime.Now.Subtract(ti0).TotalMilliseconds); - - Console.WriteLine("...reading and creating response..."); - CopyHttpResponseMessageToOutgoingResponse(response, context.OutgoingResponse); - var stream = response.Content != null ? await response.Content.ReadAsStreamAsync() : null; - var message = StreamMessageHelper.CreateMessage(MessageVersion.None, "GETRESPONSE", stream ?? new MemoryStream()); - Console.WriteLine("...and done (total time: {0:N0} ms).", DateTime.Now.Subtract(ti0).TotalMilliseconds); - return message; - } - catch (Exception ex) - { - WriteException(ex); - throw; - } - } - - private HttpRequestMessage BuildForwardedRequest(WebOperationContext context, Message msg) - { - var incomingRequest = context.IncomingRequest; - - var mappedUri = new Uri(incomingRequest.UriTemplateMatch.RequestUri.ToString().Replace(_config.RelayAddress.ToString(), _config.TargetAddress.ToString())); - var newRequest = new HttpRequestMessage(new HttpMethod(incomingRequest.Method), mappedUri); - - // Copy headers - var hostHeader = _config.TargetAddress.Host + (_config.TargetAddress.Port != 80 || _config.TargetAddress.Port != 443 ? ":" + _config.TargetAddress.Port : ""); - foreach (var name in incomingRequest.Headers.AllKeys.Where(name => !_httpContentHeaders.Contains(name))) - { - newRequest.Headers.TryAddWithoutValidation(name, name == "Host" ? hostHeader : incomingRequest.Headers.Get(name)); - } - - if (msg != null) - { - Stream messageStream = null; - if (msg.Properties.TryGetValue("WebBodyFormatMessageProperty", out var value)) - { - if (value is WebBodyFormatMessageProperty prop && (prop.Format == WebContentFormat.Json || prop.Format == WebContentFormat.Raw)) - { - messageStream = StreamMessageHelper.GetStream(msg); - } - } - else - { - var ms = new MemoryStream(); - using (var xw = XmlDictionaryWriter.CreateTextWriter(ms, Encoding.UTF8, false)) - { - msg.WriteBodyContents(xw); - } - - ms.Seek(0, SeekOrigin.Begin); - messageStream = ms; - } - - if (messageStream != null) - { - if (_config.BufferRequestContent) - { - var ms1 = new MemoryStream(); - messageStream.CopyTo(ms1); - ms1.Seek(0, SeekOrigin.Begin); - newRequest.Content = new StreamContent(ms1); - } - else - { - var ms1 = new MemoryStream(); - messageStream.CopyTo(ms1); - ms1.Seek(0, SeekOrigin.Begin); - - var debugMs = new MemoryStream(); - ms1.CopyTo(debugMs); - debugMs.Seek(0, SeekOrigin.Begin); - - var result = Encoding.UTF8.GetString(debugMs.ToArray()); - WriteJsonObject(result); - - ms1.Seek(0, SeekOrigin.Begin); - newRequest.Content = new StreamContent(ms1); - } - - foreach (var name in incomingRequest.Headers.AllKeys.Where(name => _httpContentHeaders.Contains(name))) - { - newRequest.Content.Headers.TryAddWithoutValidation(name, incomingRequest.Headers.Get(name)); - } - } - } - - return newRequest; - } - - private static void WriteException(Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(ex); - Console.WriteLine(""); - Console.ResetColor(); - } - - private static void WriteJsonObject(string result) - { - Console.ForegroundColor = ConsoleColor.Yellow; - - var formatted = result; - if (IsValidJson(result)) - { - var s = new JsonSerializerSettings - { - Formatting = Formatting.Indented - }; - - dynamic o = JsonConvert.DeserializeObject(result); - formatted = JsonConvert.SerializeObject(o, s); - } - - Console.WriteLine(formatted); - Console.ResetColor(); - } - - private static void WriteFlowerLine() - { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine("\r\n=> {0:MM/dd/yyyy hh:mm:ss.fff tt} {1}", DateTime.Now, new string('*', 80)); - Console.ResetColor(); - } - - private static void CopyHttpResponseMessageToOutgoingResponse(HttpResponseMessage response, OutgoingWebResponseContext outgoingResponse) - { - outgoingResponse.StatusCode = response.StatusCode; - outgoingResponse.StatusDescription = response.ReasonPhrase; - if (response.Content == null) - { - outgoingResponse.SuppressEntityBody = true; - } - - foreach (var kvp in response.Headers) - { - foreach (var value in kvp.Value) - { - outgoingResponse.Headers.Add(kvp.Key, value); - } - } - - if (response.Content != null) - { - foreach (var kvp in response.Content.Headers) - { - foreach (var value in kvp.Value) - { - outgoingResponse.Headers.Add(kvp.Key, value); - } - } - } - } - - private static bool IsValidJson(string strInput) - { - strInput = strInput.Trim(); - if ((!strInput.StartsWith("{") || !strInput.EndsWith("}")) && (!strInput.StartsWith("[") || !strInput.EndsWith("]"))) - { - return false; - } - - try - { - JToken.Parse(strInput); - return true; - } - catch //some other exception - { - return false; - } - } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil/Program.cs b/Src/ServiceBusRelayUtil/Program.cs deleted file mode 100644 index 9de1000..0000000 --- a/Src/ServiceBusRelayUtil/Program.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Configuration; -using System.ServiceModel.Channels; -using System.ServiceModel.Web; -using Microsoft.ServiceBus; - -namespace GaboG.ServiceBusRelayUtil -{ - internal class Program - { - // https://github.com/pmhsfelix/WebApi.Explorations.ServiceBusRelayHost - // https://docs.microsoft.com/en-us/azure/service-bus-relay/service-bus-relay-rest-tutorial - private static void Main() - { - var relayNamespace = ConfigurationManager.AppSettings["RelayNamespace"]; - var relayAddress = ServiceBusEnvironment.CreateServiceUri("https", relayNamespace, ConfigurationManager.AppSettings["RelayName"]); - - var config = new ServiceBusRelayUtilConfig - { - RelayAddress = relayAddress, - RelayPolicyName = ConfigurationManager.AppSettings["PolicyName"], - RelayPolicyKey = ConfigurationManager.AppSettings["PolicyKey"], - MaxReceivedMessageSize = long.Parse(ConfigurationManager.AppSettings["MaxReceivedMessageSize"]), - TargetAddress = new Uri(ConfigurationManager.AppSettings["TargetServiceAddress"]) - }; - - var host = CreateWebServiceHost(config, relayAddress); - host.Open(); - - Console.WriteLine("Azure Service Bus is listening at \n\r\t{0}\n\rrouting requests to \n\r\t{1}\n\r\n\r", relayAddress, config.TargetAddress); - Console.WriteLine(); - Console.WriteLine("Press [Enter] to exit"); - Console.ReadLine(); - - host.Close(); - } - - private static WebServiceHost CreateWebServiceHost(ServiceBusRelayUtilConfig config, Uri address) - { - var host = new WebServiceHost(new DispatcherService(config)); - var binding = GetBinding(config.MaxReceivedMessageSize); - var endpoint = host.AddServiceEndpoint(typeof(DispatcherService), binding, address); - var behavior = GetTransportBehavior(config.RelayPolicyName, config.RelayPolicyKey); - endpoint.Behaviors.Add(behavior); - return host; - } - - private static Binding GetBinding(long maxReceivedMessageSize) - { - var webHttpRelayBinding = new WebHttpRelayBinding(EndToEndWebHttpSecurityMode.None, RelayClientAuthenticationType.None) - { - MaxReceivedMessageSize = maxReceivedMessageSize - }; - var bindingElements = webHttpRelayBinding.CreateBindingElements(); - var webMessageEncodingBindingElement = bindingElements.Find(); - webMessageEncodingBindingElement.ContentTypeMapper = new RawContentTypeMapper(); - return new CustomBinding(bindingElements); - } - - private static TransportClientEndpointBehavior GetTransportBehavior(string keyName, string sharedAccessKey) - { - return new TransportClientEndpointBehavior - { - TokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(keyName, sharedAccessKey) - }; - } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil/Properties/AssemblyInfo.cs b/Src/ServiceBusRelayUtil/Properties/AssemblyInfo.cs deleted file mode 100644 index 7ac5c7a..0000000 --- a/Src/ServiceBusRelayUtil/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("ServiceBusRelayUtil")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("ServiceBusRelayUtil")] -[assembly: AssemblyCopyright("Copyright © 2017")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("b9da41e3-4e0a-41d6-b7d1-64ab017d4fed")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Src/ServiceBusRelayUtil/RawContentTypeMapper.cs b/Src/ServiceBusRelayUtil/RawContentTypeMapper.cs deleted file mode 100644 index 40289c4..0000000 --- a/Src/ServiceBusRelayUtil/RawContentTypeMapper.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.ServiceModel.Channels; - -namespace GaboG.ServiceBusRelayUtil -{ - internal class RawContentTypeMapper : WebContentTypeMapper - { - public override WebContentFormat GetMessageFormatForContentType(string contentType) - { - return WebContentFormat.Raw; - } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil/ServiceBusRelayUtil.csproj b/Src/ServiceBusRelayUtil/ServiceBusRelayUtil.csproj deleted file mode 100644 index bc6f885..0000000 --- a/Src/ServiceBusRelayUtil/ServiceBusRelayUtil.csproj +++ /dev/null @@ -1,143 +0,0 @@ - - - - - Debug - AnyCPU - {B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED} - Exe - GaboG.ServiceBusRelayUtil - ServiceBusRelayUtil - v4.7 - 512 - true - - - - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - App.ico - - - - ..\packages\Microsoft.Azure.Amqp.2.3.7\lib\net45\Microsoft.Azure.Amqp.dll - - - ..\packages\Microsoft.Azure.ServiceBus.3.3.0\lib\net461\Microsoft.Azure.ServiceBus.dll - - - ..\packages\Microsoft.Azure.Services.AppAuthentication.1.0.3\lib\net452\Microsoft.Azure.Services.AppAuthentication.dll - - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.19.8\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll - - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.19.8\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll - - - ..\packages\Microsoft.IdentityModel.Logging.5.2.2\lib\net451\Microsoft.IdentityModel.Logging.dll - - - ..\packages\Microsoft.IdentityModel.Tokens.5.2.2\lib\net451\Microsoft.IdentityModel.Tokens.dll - - - ..\packages\WindowsAzure.ServiceBus.5.1.0\lib\net46\Microsoft.ServiceBus.dll - - - ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll - - - - - - - ..\packages\System.Diagnostics.DiagnosticSource.4.4.1\lib\net46\System.Diagnostics.DiagnosticSource.dll - - - ..\packages\System.IdentityModel.Tokens.Jwt.5.2.2\lib\net451\System.IdentityModel.Tokens.Jwt.dll - - - ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll - - - ..\packages\System.Net.WebSockets.4.3.0\lib\net46\System.Net.WebSockets.dll - - - ..\packages\System.Net.WebSockets.Client.4.3.2\lib\net46\System.Net.WebSockets.Client.dll - - - ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll - - - - ..\packages\System.Runtime.Serialization.Primitives.4.3.0\lib\net46\System.Runtime.Serialization.Primitives.dll - - - ..\packages\System.Runtime.Serialization.Xml.4.3.0\lib\net46\System.Runtime.Serialization.Xml.dll - - - ..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll - - - ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll - - - ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll - - - ..\packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll - - - - - - - - - - - - - - - - - - - - - - Designer - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil/ServiceBusRelayUtilConfig.cs b/Src/ServiceBusRelayUtil/ServiceBusRelayUtilConfig.cs deleted file mode 100644 index f4944e4..0000000 --- a/Src/ServiceBusRelayUtil/ServiceBusRelayUtilConfig.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace GaboG.ServiceBusRelayUtil -{ - public class ServiceBusRelayUtilConfig - { - public string RelayPolicyName { get; set; } - public string RelayPolicyKey { get; set; } - public Uri RelayAddress { get; set; } - - public bool BufferRequestContent { get; set; } - public long MaxReceivedMessageSize { get; set; } - public Uri TargetAddress { get; set; } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtil/azureservicebusrelaylogo_150.png b/Src/ServiceBusRelayUtil/azureservicebusrelaylogo_150.png deleted file mode 100644 index 3fe665a..0000000 Binary files a/Src/ServiceBusRelayUtil/azureservicebusrelaylogo_150.png and /dev/null differ diff --git a/Src/ServiceBusRelayUtil/packages.config b/Src/ServiceBusRelayUtil/packages.config deleted file mode 100644 index 79eb4aa..0000000 --- a/Src/ServiceBusRelayUtil/packages.config +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Src/ServiceBusRelayUtilNetCore/App.ico b/Src/ServiceBusRelayUtilNetCore/App.ico deleted file mode 100644 index f3c2e20..0000000 Binary files a/Src/ServiceBusRelayUtilNetCore/App.ico and /dev/null differ diff --git a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs b/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs deleted file mode 100644 index 1704890..0000000 --- a/Src/ServiceBusRelayUtilNetCore/DispatcherService.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using GaboG.ServiceBusRelayUtilNetCore.Extensions; -using Microsoft.Azure.Relay; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace GaboG.ServiceBusRelayUtilNetCore - -{ - internal class DispatcherService - { - private readonly HttpClient _httpClient; - private readonly string _hybridConnectionSubPath; - private readonly HybridConnectionListener _listener; - private readonly Uri _targetServiceAddress; - - public DispatcherService(string relayNamespace, string connectionName, string keyName, string key, Uri targetServiceAddress) - { - _targetServiceAddress = targetServiceAddress; - - var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(keyName, key); - _listener = new HybridConnectionListener(new Uri($"sb://{relayNamespace}/{connectionName}"), tokenProvider); - - _httpClient = new HttpClient - { - BaseAddress = targetServiceAddress - }; - _httpClient.DefaultRequestHeaders.ExpectContinue = false; - - _hybridConnectionSubPath = _listener.Address.AbsolutePath.EnsureEndsWith("/"); - } - - public async Task OpenAsync(CancellationToken cancelToken) - { - _listener.RequestHandler = ListenerRequestHandler; - await _listener.OpenAsync(cancelToken); - Console.WriteLine("Azure Service Bus is listening on \n\r\t{0}\n\rand routing requests to \n\r\t{1}\n\r\n\r", _listener.Address, _httpClient.BaseAddress); - Console.WriteLine("Press [Enter] to exit"); - } - - public Task CloseAsync(CancellationToken cancelToken) - { - _httpClient.Dispose(); - return _listener.CloseAsync(cancelToken); - } - - private async void ListenerRequestHandler(RelayedHttpListenerContext context) - { - var startTimeUtc = DateTime.UtcNow; - try - { - Console.WriteLine("Calling {0}...", _targetServiceAddress); - var requestMessage = CreateHttpRequestMessage(context); - var responseMessage = await _httpClient.SendAsync(requestMessage); - await SendResponseAsync(context, responseMessage); - await context.Response.CloseAsync(); - } - - catch (Exception ex) - { - LogException(ex); - SendErrorResponse(ex, context); - } - finally - { - LogRequest(startTimeUtc); - } - } - - 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 HttpRequestMessage CreateHttpRequestMessage(RelayedHttpListenerContext context) - { - var requestMessage = new HttpRequestMessage(); - if (context.Request.HasEntityBody) - { - requestMessage.Content = new StreamContent(context.Request.InputStream); - // Experiment to see if I can capture the return message instead of having the bot responding directly (so far it doesn't work). - //var contentStream = new MemoryStream(); - //var writer = new StreamWriter(contentStream); - //var newActivity = requestMessage.Content.ReadAsStringAsync().Result.Replace("https://directline.botframework.com/", "https://localhost:44372/"); - //writer.Write(newActivity); - //writer.Flush(); - //contentStream.Position = 0; - //requestMessage.Content = new StreamContent(contentStream); - 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]); - } - - LogRequestActivity(requestMessage); - - 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}"); - //Console.WriteLine(buffer); - - Console.WriteLine("...and back {0:N0} ms...", stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds); - Console.WriteLine(""); - } - - private void LogRequestActivity(HttpRequestMessage requestMessage) - { - var content = requestMessage.Content.ReadAsStringAsync().Result; - Console.ForegroundColor = ConsoleColor.Yellow; - - var formatted = content; - if (IsValidJson(formatted)) - { - var s = new JsonSerializerSettings - { - Formatting = Formatting.Indented - }; - - dynamic o = JsonConvert.DeserializeObject(content); - formatted = JsonConvert.SerializeObject(o, s); - } - - Console.WriteLine(formatted); - Console.ResetColor(); - } - - private static void LogException(Exception ex) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine(ex); - Console.WriteLine(""); - 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; - } - } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtilNetCore/Program.cs b/Src/ServiceBusRelayUtilNetCore/Program.cs deleted file mode 100644 index e4ba1b7..0000000 --- a/Src/ServiceBusRelayUtilNetCore/Program.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; - -// https://docs.microsoft.com/en-us/azure/service-bus-relay/service-bus-relay-rest-tutorial -// https://github.com/Azure/azure-relay-dotnet -// https://docs.microsoft.com/en-us/azure/service-bus-relay/relay-hybrid-connections-http-requests-dotnet-get-started - -// This is what I think I need -// https://github.com/Azure/azure-relay/blob/master/samples/hybrid-connections/dotnet/hcreverseproxy/README.md -// https://github.com/Azure/azure-relay/tree/master/samples/hybrid-connections/dotnet/hcreverseproxy - -// Publish -// https://stackoverflow.com/questions/44074121/build-net-core-console-application-to-output-an-exe -// https://docs.microsoft.com/en-us/dotnet/core/rid-catalog - -namespace GaboG.ServiceBusRelayUtilNetCore -{ - public class Program - { - public static IConfiguration Configuration { get; set; } - - public static void Main(string[] args) - { - var builder = new ConfigurationBuilder() - .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) - .AddJsonFile("appsettings.json", true, true) - .AddEnvironmentVariables(); - Configuration = builder.Build(); - - RunAsync().GetAwaiter().GetResult(); - } - - static async Task RunAsync() - { - var relayNamespace = Configuration["RelayNamespace"]; - var connectionName = Configuration["RelayName"]; - var keyName = Configuration["PolicyName"]; - var key = Configuration["PolicyKey"]; - var targetServiceAddress = new Uri(Configuration["TargetServiceAddress"]); - - var hybridProxy = new DispatcherService(relayNamespace, connectionName, keyName, key, targetServiceAddress); - - await hybridProxy.OpenAsync(CancellationToken.None); - - Console.ReadLine(); - - await hybridProxy.CloseAsync(CancellationToken.None); - } - } -} \ No newline at end of file diff --git a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj b/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj deleted file mode 100644 index 2d26a18..0000000 --- a/Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - Exe - netcoreapp2.1 - App.ico - GaboG.ServiceBusRelayUtilNetCore - - - - - - - - - - - Always - - - - - - - - - - - diff --git a/Src/ServiceBusRelayUtilNetCore/appsettings.json b/Src/ServiceBusRelayUtilNetCore/appsettings.json deleted file mode 100644 index 6429582..0000000 --- a/Src/ServiceBusRelayUtilNetCore/appsettings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "RelayNamespace": "[Your Namespace].servicebus.windows.net", - "RelayName": "[Your Relay Name]", - "PolicyName": "[Your Shared Access Policy Name]", - "PolicyKey": "[Your Policy's Key]", - "TargetServiceAddress": "http://localhost:[PORT]" -} diff --git a/docs/images/WebSocketConsole.png b/docs/images/WebSocketConsole.png new file mode 100644 index 0000000..aa1677d Binary files /dev/null and b/docs/images/WebSocketConsole.png differ diff --git a/docs/images/passage.png b/docs/images/passage.png new file mode 100644 index 0000000..912460a Binary files /dev/null and b/docs/images/passage.png differ diff --git a/docs/passage.drawio b/docs/passage.drawio new file mode 100644 index 0000000..a384d59 --- /dev/null +++ b/docs/passage.drawio @@ -0,0 +1 @@ +7Vpdc5s4FP01eVwPiC/7MbbjZjvpbqbJbHefPAqWQRuBGCF/9devhIQNCDs0MU67bTvjoIsEks49R1cXXTmTZPuBwSz+RBeIXAFrsb1yplcA2NZwKP5Iy05ZRr42RAwvdKWD4QF/RWVLbV3hBcprFTmlhOOsbgxpmqKQ12yQMbqpV1tSUn9rBiNkGB5CSEzrF7zgsbbalnW4cYtwFOtXDz19I4FlZW3IY7igm4rJublyJoxSrq6S7QQROXnlvKh2syN39x1jKOVdGuB7+8/wI/NoMMo/rNzZx+jx82/AV49ZQ7LSI/4LM76CsuUfiG8oexZXlImfOxrWzcWo+K6cqnyDEwJTURovMSETSkQzeccB9mzm+sKuX4YYR9ujw7D3kyO8CtEEcbYTVcoGI6CaaIdySyQ2B3jcQA8rriBj+7oi1C4R7Z99mDVxoSfuWybRmMOxABVYD4itcYiMiYJ5plx1ibdIPHgc80S8cGqLy4zilBcd9MZX3lRYIMFRKgyhmCAkJnSMk8Jnx0uack0YGxzsU5xEYhgEP4lf+HXFkBwexPMEhjFO0ZwgyFKcykqin3Pdy3yQr6OuEJ3wpaPAOeX8a+A8EzdvaMJW2s6Omm0bsH1BTzkNn5EE73b3JBUK+ER0ZPwkCOBH8uoO5xylAogmqgVyiN2skQJQornnvCUKC5jHEu6iUIE854w+o5IuKS0YROATIvc0xxzTGvoSGSyIeNeo8EQ5p0mlwrX2G06zNi9qUNSz5H/V5UyOKNlGUtEHSR5CNBBDW6VoEKtJEWPBTLiwejOCOe/TccQi8qLjWC2O0xfd7ZGBPVqIRUMXNYCMrtLFHm7KeEwjmgrcqMSjAP5fxPlOMxiuOG26BWT8Wi5hEjYC8xyHpXmGSVlN9UV24DWTLkZBVyxEp4YL2tFhiECO1/XXtk22bnov+VGRA6+OauDVnyCGGSGuGzUQ2/fiDSCaon37+Hhv8n1CMNJdqCJOV5wIMZ3sww5LK3J12XPATHZpLJi0kE9pcLxO/GqDBjmXxb+mhJxLCSo+11xSirIetHylsXI11QJucnfAkHKq30PZn7Eoqqt6rZjzbJ4xymlISZ/6Aey6p7meoR9+i370Fi6U3flZ9KMM2wtC9yk0bwviTEEwYSJE7DtQxe9DQleSB5sYc/SQwWImNsLJG7yq89kfjW9msxZhqHJ9HzvYXclxOnD2gnr85bbEzXYLD5z+4marxylvBlBMjehl1Sz1kaAlPyqi3wOgwHJqgPotgII2QEFvyuacVLaDiN0crG8XOjFdbPe3bD8YlcV/iqJXFqdb/XhV2lVL94hhMXy5FBbG8+mm3VEhHaejQgbvKZC2a4B7TugCr45dMHoNeGdEpeuydST4uBAohoJ+ehDlRwST3IDrvDvUhgROZ8OJCy6xc20NvZuB6BqJKQCDBIeM5nTJ57yYkrMI79Ctr6TAAobyBm0RJejJCxzDC65l3qeagrLGq9zc3HwWTroz/OSC+akiuyD+6n7ORS/nRad0NqpwpjEMn6NCaxoueAYsnVEdS9sxV9HL7g68i+wO0BZzJbzACnRZCa/vjHT5ILyysKsUepRdv6PsqkXzvXTXTJuXjLuemjS7zjJx5zOKsFBSWMheE+MXxbGCXUed7KLaliUCV9C2kZfDUb9zGMoJnaucH5Vpl3MwL3D9OvPsbgnh/pjnG6j0wrx9zPNDBqtBR36O3pOegUHPW86zX1n9k1n9t1PaHTYDo6FJ6Yum6oOLUPryLHwru47k5IFbw891G8D0nJQfGbTt8kXuSIb+J2Ju/XvcBnX/HvdCcGx9Z3x2Wva75Q5Pxl4qBvuBQiucCpFIQ9RMD/a+73HtevTltByjAC3Rl9MbtGb+/Re0rwusG6vwPt/7btCa+Ql1QEanqawWTY9hmsqjZM0bn1Cew6g4vWLdpItsvxS99wGbVJ2KUudq7hleQ47mZQ91NuMb/ODYCapTHxjO4Du+ZzcU3/SdAJi+4/t9+Y5r+M7JkO5/9Y0BpQuzkjD2E1N2TXi/OfPSHnwOnUaw4TVOhKgBGMGn8SAPeKcf9OooVhQP5zVV9cOpV+fmPw== \ No newline at end of file diff --git a/Src/ServiceBusRelayUtilNetCore/Extensions/StringEx.cs b/src/Microsoft.HybridConnections.Core/Extensions/StringEx.cs similarity index 92% rename from Src/ServiceBusRelayUtilNetCore/Extensions/StringEx.cs rename to src/Microsoft.HybridConnections.Core/Extensions/StringEx.cs index e11dc7c..78a63ca 100644 --- a/Src/ServiceBusRelayUtilNetCore/Extensions/StringEx.cs +++ b/src/Microsoft.HybridConnections.Core/Extensions/StringEx.cs @@ -3,7 +3,7 @@ using System; -namespace GaboG.ServiceBusRelayUtilNetCore.Extensions +namespace Microsoft.ServiceBusBotRelay.Core.Extensions { public static class StringEx { diff --git a/src/Microsoft.HybridConnections.Core/Extensions/WebHeaderCollectionExtensions.cs b/src/Microsoft.HybridConnections.Core/Extensions/WebHeaderCollectionExtensions.cs new file mode 100644 index 0000000..7d0aadb --- /dev/null +++ b/src/Microsoft.HybridConnections.Core/Extensions/WebHeaderCollectionExtensions.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Microsoft.HybridConnections.Core.Extensions +{ + public static class WebHeaderCollectionExtensions + { + public static IEnumerable> GetHeaders(this System.Net.WebHeaderCollection webHeaderCollection) + { + string[] keys = webHeaderCollection.AllKeys; + for (int i = 0; i < keys.Length; i++) + { + yield return new KeyValuePair(keys[i], webHeaderCollection[keys[i]]); + } + } + } +} diff --git a/src/Microsoft.HybridConnections.Core/HttpListener.cs b/src/Microsoft.HybridConnections.Core/HttpListener.cs new file mode 100644 index 0000000..ba64e3f --- /dev/null +++ b/src/Microsoft.HybridConnections.Core/HttpListener.cs @@ -0,0 +1,217 @@ +using Microsoft.Azure.Relay; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.HybridConnections.Core +{ + public class HttpListener + { + private readonly HttpClient _httpClient = null; + private readonly HybridConnectionListener _listener; + + public CancellationTokenSource CTS { get; set; } + + /// + /// The constructor + /// + /// + /// + /// + /// + /// + /// + /// + public HttpListener(string relayNamespace, string connectionName, string keyName, string key, string targetServiceAddress, Action eventHandler, CancellationTokenSource cts) + { + CTS = cts; + + var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(keyName, key); + _listener = new HybridConnectionListener(new Uri($"sb://{relayNamespace}/{connectionName}"), tokenProvider); + + // Subscribe to the status events. + _listener.Connecting += (o, e) => { eventHandler("connecting"); }; + _listener.Offline += (o, e) => { eventHandler("offline"); }; + _listener.Online += (o, e) => { eventHandler("online"); }; + + if (!string.IsNullOrEmpty(targetServiceAddress)) + { + // Send the request message via Http + _httpClient = new HttpClient { BaseAddress = new Uri(targetServiceAddress, UriKind.RelativeOrAbsolute) }; + _httpClient.DefaultRequestHeaders.ExpectContinue = false; + } + } + + + /// + /// Convert RelayedHttpListenerContext into HttpRequestMessage + /// + /// + /// + /// + public static async Task CreateHttpRequestMessageAsync(RelayedHttpListenerContext context, string connectionName) + { + 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($"/{connectionName}/", 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 Logger.LogRequestActivityAsync(requestMessage); + + //var requestMessageSer = await RelayedHttpListenerRequestSerializer.SerializeAsync(requestMessage); + //var deserializedRequestMessage = RelayedHttpListenerRequestSerializer.Deserialize(requestMessageSer); + + return requestMessage; + } + + + /// + /// Sends the response to the server + /// + /// + /// + /// + public static 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); + } + + + /// + /// Sends the error response + /// + /// + /// + public static void SendErrorResponse(Exception ex, RelayedHttpListenerContext context) + { + context.Response.StatusCode = HttpStatusCode.InternalServerError; + context.Response.StatusDescription = $"Http Listener: Internal Server Error: {ex.GetType().FullName}: {ex.Message}"; + context.Response.Close(); + } + + /// + // Opening the listener establishes the control channel to + // the Azure Relay service. The control channel is continuously + // maintained, and is reestablished when connectivity is disrupted. + /// + /// + /// + public async Task OpenAsync(Action relayHandler) + { + _listener.RequestHandler = relayHandler; + await _listener.OpenAsync(CTS.Token); + + // Provide callback for a cancellation token that will close the listener. + CTS.Token.Register(() => _listener.CloseAsync(CancellationToken.None)); + } + + /// + /// Starts listening to the messages + /// + /// + public async Task ListenAsync() + { + // Start a new thread that will continuously read the console. + await Console.In.ReadLineAsync().ContinueWith((s) => { CTS.Cancel(); }); + + // Close the listener + await _listener.CloseAsync(); + } + + + /// + /// Closes the listener after you exit the processing loop + /// + /// + /// + public Task CloseAsync() + { + if (_httpClient != null ) _httpClient.Dispose(); + return _listener.CloseAsync(CTS.Token); + } + + + /// + /// Creates and sends the Http Request message + /// + /// + /// + public async Task SendHttpRequestMessageAsync(RelayedHttpListenerContext context, string connectionName) + { + 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($"/{connectionName}/", 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 Logger.LogRequestActivityAsync(requestMessage); + + //var requestMessageSer = await RelayedHttpListenerRequestSerializer.SerializeAsync(requestMessage); + //var deserializedRequestMessage = RelayedHttpListenerRequestSerializer.Deserialize(requestMessageSer); + + // Send the request message via Http + return (_httpClient != null) ? await _httpClient.SendAsync(requestMessage) : null; + } + } +} diff --git a/src/Microsoft.HybridConnections.Core/Logger.cs b/src/Microsoft.HybridConnections.Core/Logger.cs new file mode 100644 index 0000000..723dddd --- /dev/null +++ b/src/Microsoft.HybridConnections.Core/Logger.cs @@ -0,0 +1,167 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.HybridConnections.Core +{ + public static class Logger + { + public static List Logs { get; private set; } = new List(); + public static bool IsVerboseLogs { get; set; } + public static int MaxRows { get; set; } + public static int LeftPad { get; set; } + public static int MidPad { get; set; } + + /// + /// Log Request message + /// + /// + /// + /// + /// + public static void LogRequest(string requestType, string requestAddress, string statusCode, string message) + { + var leftSection = $"{DateTime.UtcNow.ToString("s", CultureInfo.InvariantCulture)}: {requestType} {requestAddress}"; + var filler = string.Empty.PadRight((LeftPad - leftSection.Length > 0 ? LeftPad - leftSection.Length : 0) + MidPad); + + Logs.Add($"{leftSection}{filler}{statusCode} {message}"); + + if (Logs.Count >= MaxRows) + { + Logs.RemoveAt(0); + } + } + + + /// + /// Log Request message + /// + /// + /// + /// + /// + /// + public static void LogRequest(string requestType, string requestAddress, string statusCode, string message, Action logsHandler) + { + LogRequest(requestType, requestAddress, statusCode, message); + logsHandler(); + } + + + /// + /// Clear logs + /// + public static void ClearLogs() + { + Logs.Clear(); + } + + /// + /// Logs the request activity + /// + /// + public static async Task LogRequestActivityAsync(HttpRequestMessage requestMessage) + { + if (requestMessage.Content == null || !IsVerboseLogs) return false; + + try + { + var content = await requestMessage.Content.ReadAsStringAsync(); + 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); + } + + Console.WriteLine(formatted); + Console.ResetColor(); + + return true; + } + catch (Exception) + { + return false; + } + } + + /// + /// Logs the exception + /// + /// + public static void LogException(Exception ex) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(ex); + Console.WriteLine(""); + Console.ResetColor(); + } + + + /// + /// Validates the Json string + /// + /// + /// + 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; + } + } + + /// + /// OutputRequestAsync + /// + /// + /// + public static async Task OutputRequestAsync(string messageSent) + { + if (!IsVerboseLogs) return; + + var activityRequest = RelayedHttpListenerRequestSerializer.Deserialize(messageSent); + await LogRequestActivityAsync(activityRequest); + } + + /// + /// Logs the request's starting time + /// + /// + public static void LogPerformanceMetrics(DateTime startTimeUtc) + { + if (!IsVerboseLogs) return; + + var stopTimeUtc = DateTime.UtcNow; + + Logs.Add($"and back {stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds} ms..."); + + if (Logs.Count >= MaxRows) + { + Logs.RemoveAt(0); + } + } + } +} diff --git a/src/Microsoft.HybridConnections.Core/Microsoft.HybridConnections.Core.csproj b/src/Microsoft.HybridConnections.Core/Microsoft.HybridConnections.Core.csproj new file mode 100644 index 0000000..758d114 --- /dev/null +++ b/src/Microsoft.HybridConnections.Core/Microsoft.HybridConnections.Core.csproj @@ -0,0 +1,12 @@ + + + + net5.0 + + + + + + + + diff --git a/src/Microsoft.HybridConnections.Core/RelayedHttpListenerRequestSerializer.cs b/src/Microsoft.HybridConnections.Core/RelayedHttpListenerRequestSerializer.cs new file mode 100644 index 0000000..d23aa67 --- /dev/null +++ b/src/Microsoft.HybridConnections.Core/RelayedHttpListenerRequestSerializer.cs @@ -0,0 +1,141 @@ +using Microsoft.Azure.Relay; +using Microsoft.HybridConnections.Core.Extensions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Microsoft.HybridConnections.Core +{ + public static class RelayedHttpListenerRequestSerializer + { + /// + /// Serialize RelayedHttpListenerRequest + /// + /// + /// + public static async Task SerializeAsync(RelayedHttpListenerRequest request) + { + var requestMessage = new RequestMessage + { + Content = await (new StreamContent(request.InputStream)).ReadAsByteArrayAsync(), + HttpMethod = request.HttpMethod, + RemoteEndPoint = request.RemoteEndPoint.Address.ToString(), + Url = request.Url.AbsoluteUri, + HybridConnectionScheme = request.Url.Scheme, + HybridConnectionName = request.Url.Segments[1].Trim('/') + }; + + requestMessage.Headers = requestMessage.Headers ?? new List>>(); + foreach (var header in request.Headers.GetHeaders()) + { + ((List>>)requestMessage.Headers) + .Add(new KeyValuePair>(header.Key, new List { header.Value })); + + } + + return JsonConvert.SerializeObject(requestMessage); + } + + /// + /// Serialize HttpRequestMessage + /// + /// + /// + public static async Task SerializeAsync(HttpRequestMessage request) + { + var requestMessage = new RequestMessage + { + Content = await request.Content.ReadAsByteArrayAsync(), + HttpMethod = request.Method.Method, + RemoteEndPoint = request.RequestUri.ToString(), + Url = request.RequestUri.ToString() + }; + + // populate Headers + foreach (var header in request.Headers) + { + if (string.Equals(header.Key, "Host", StringComparison.OrdinalIgnoreCase) || + string.Equals(header.Key, "Content-Type", StringComparison.OrdinalIgnoreCase)) + { + // Don't flow these headers here + continue; + } + requestMessage.Headers = requestMessage.Headers ?? new List>>(); + ((List>>)requestMessage.Headers) + .Add(new KeyValuePair>(header.Key, header.Value)); + } + + return JsonConvert.SerializeObject(requestMessage); + } + + /// + /// Deserialize the JSON into the HttpRequestMessage + /// + /// + /// + public static HttpRequestMessage Deserialize(string jsonObject) + { + var serializedRequestMessage = JsonConvert.DeserializeObject(jsonObject); + + var requestMessage = new HttpRequestMessage(); + // Get message content + requestMessage.Content = new ByteArrayContent(serializedRequestMessage.Content); + + // populate Headers + foreach (var header in serializedRequestMessage.Headers) + { + if (string.Equals(header.Key, "Host", StringComparison.OrdinalIgnoreCase) || + string.Equals(header.Key, "Content-Type", StringComparison.OrdinalIgnoreCase)) + { + // Don't flow these headers here + continue; + } + requestMessage.Headers.Add(header.Key, header.Value); + } + + requestMessage.Method = new HttpMethod(serializedRequestMessage.HttpMethod); + requestMessage.RequestUri = string.IsNullOrEmpty(serializedRequestMessage.HybridConnectionScheme) ? + new Uri(serializedRequestMessage.Url, UriKind.RelativeOrAbsolute) : + GenerateUriFromSbUrl(serializedRequestMessage.Url, serializedRequestMessage.HybridConnectionScheme, serializedRequestMessage.HybridConnectionName); + + return requestMessage; + + } + + /// + /// Validates the Json string + /// + /// + /// + 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; + } + } + + + private static Uri GenerateUriFromSbUrl(string sbUrl, string scheme, string connectionName) + { + var httpUri = new Uri(sbUrl.Replace($"{scheme}://", "http://")); + var relativePath = httpUri.GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped); + relativePath = relativePath.Replace($"/{connectionName}/", string.Empty, StringComparison.OrdinalIgnoreCase); + return new Uri(relativePath, UriKind.RelativeOrAbsolute); + } + } +} diff --git a/src/Microsoft.HybridConnections.Core/RequestMessage.cs b/src/Microsoft.HybridConnections.Core/RequestMessage.cs new file mode 100644 index 0000000..07c0273 --- /dev/null +++ b/src/Microsoft.HybridConnections.Core/RequestMessage.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.HybridConnections.Core +{ + [Serializable] + public class RequestMessage + { + public IEnumerable>> Headers { get; set; } + public byte[] Content { get; set; } + public string HttpMethod { get; set; } + public string RemoteEndPoint { get; set; } + public string Url { get; set; } + public string HybridConnectionScheme { get; set; } + public string HybridConnectionName { get; set; } + } +} diff --git a/src/Microsoft.HybridConnections.Core/WebSocketListener.cs b/src/Microsoft.HybridConnections.Core/WebSocketListener.cs new file mode 100644 index 0000000..2cb1383 --- /dev/null +++ b/src/Microsoft.HybridConnections.Core/WebSocketListener.cs @@ -0,0 +1,106 @@ +using Microsoft.Azure.Relay; +using Microsoft.ServiceBusBotRelay.Core.Extensions; +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.HybridConnections.Core +{ + public class WebSocketListener + { + private readonly HybridConnectionListener _listener; + + public CancellationTokenSource CTS { get; set; } + + /// + /// The constructor + /// + /// + /// + /// + /// + /// + /// + public WebSocketListener(string relayNamespace, string connectionName, string keyName, string key, Action eventHandler, CancellationTokenSource cts) + { + CTS = cts; + + var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(keyName, key); + _listener = new HybridConnectionListener(new Uri($"sb://{relayNamespace}/{connectionName}"), tokenProvider); + + // Subscribe to the status events. + _listener.Connecting += (o, e) => { eventHandler("connecting"); }; + _listener.Offline += (o, e) => { eventHandler("offline"); }; + _listener.Online += (o, e) => { eventHandler("online"); }; + } + + /// + // Opening the listener establishes the control channel to + // the Azure Relay service. The control channel is continuously + // maintained, and is reestablished when connectivity is disrupted. + /// + /// + public async Task OpenAsync() + { + await _listener.OpenAsync(CTS.Token); + + // trigger cancellation when the user presses enter. Not awaited. +#pragma warning disable CS4014 + CTS.Token.Register(() => _listener.CloseAsync(CancellationToken.None)); + Task.Run(() => Console.In.ReadLineAsync().ContinueWith((s) => { CTS.Cancel(); })); +#pragma warning restore CS4014 + } + + + /// + /// Listener is ready to accept connections after it creates an outbound WebSocket connection + /// + /// + /// + public async Task ListenAsync(Action relayProcessHandler) + { + while (true) + { + // Accept the next available, pending connection request. + // Shutting down the listener will allow a clean exit with + // this method returning null + var relayConnection = await _listener.AcceptConnectionAsync(); + if (relayConnection == null) + { + break; + } + + // The following task processes a new session. We turn off the + // warning here since we intentially don't 'await' + // this call, but rather let the task handling the connection + // run out on its own without holding for it +#pragma warning disable CS4014 + Task.Run(() => + { + // Initiate the connection and process messages + relayProcessHandler(relayConnection, CTS); + }); +#pragma warning restore CS4014 + } + + // close the listener after we exit the processing loop + await _listener.CloseAsync(CTS.Token); + } + + + + /// + /// Closes the listener after you exit the processing loop + /// + /// + /// + public Task CloseAsync() + { + return _listener.CloseAsync(CTS.Token); + } + } +} diff --git a/src/Microsoft.HybridConnections.Relay/Microsoft.HybridConnections.Relay.csproj b/src/Microsoft.HybridConnections.Relay/Microsoft.HybridConnections.Relay.csproj new file mode 100644 index 0000000..a0841a0 --- /dev/null +++ b/src/Microsoft.HybridConnections.Relay/Microsoft.HybridConnections.Relay.csproj @@ -0,0 +1,31 @@ + + + + Exe + net5.0 + + + + + + + + + PreserveNewest + + + + + + + + + + + + + + + + + diff --git a/src/Microsoft.HybridConnections.Relay/Program.cs b/src/Microsoft.HybridConnections.Relay/Program.cs new file mode 100644 index 0000000..74f6e4b --- /dev/null +++ b/src/Microsoft.HybridConnections.Relay/Program.cs @@ -0,0 +1,155 @@ +using Microsoft.Azure.Relay; +using Microsoft.Extensions.Configuration; +using Microsoft.HybridConnections.Core; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.HybridConnections.Relay +{ + class Program + { + public static HttpListener HttpRelayListener { get; set; } + + public static WebsocketClient WebsocketClient { get; set; } + + private static string ListenerNamespace; + private static string ListenerConnectionName; + private static string ListenerKeyName; + private static string ListenerPolicyName; + private static string ListenerTargetAddress; + + private static string RelayNamespace; + private static string RelayConnectionName; + private static string RelayKeyName; + private static string RelayPolicyName; + + + static void Main(string[] args) + { + var builder = new ConfigurationBuilder() + .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) + .AddJsonFile("appsettings.json", false, true) + .AddEnvironmentVariables(); + var configuration = builder.Build(); + + Logger.IsVerboseLogs = bool.Parse(configuration["Log:Verbose"]); + + ListenerNamespace = $"{configuration["Listener:Namespace"]}.servicebus.windows.net"; + ListenerConnectionName = configuration["Listener:ConnectionName"]; + ListenerKeyName = configuration["Listener:PolicyName"]; + ListenerPolicyName = configuration["Listener:PolicyKey"]; + ListenerTargetAddress = configuration["Listener:TargetServiceAddress"]; + + RelayNamespace = $"{configuration["Relay:Namespace"]}.servicebus.windows.net"; + RelayConnectionName = configuration["Relay:ConnectionName"]; + RelayKeyName = configuration["Relay:PolicyName"]; + RelayPolicyName = configuration["Relay:PolicyKey"]; + + var retryDelay = Int32.Parse(configuration["Relay:RetryFrequency"]); + + bool cancelConnection = false; + + do + { + cancelConnection = RunAsync().GetAwaiter().GetResult(); + Console.WriteLine($"Retrying to connect in {retryDelay} milliseconds..."); + Thread.Sleep(retryDelay); // sleep for configurable time (in millisec) and then re-try connection again + } while (!cancelConnection); + } + + static async Task RunAsync() + { + try + { + // Create the Http hybrid proxy listener + // Create Http bi-directional connection with the Http bound target + HttpRelayListener = new HttpListener( + ListenerNamespace, + ListenerConnectionName, + ListenerKeyName, + ListenerPolicyName, + "", + HttpEventHandler, + new CancellationTokenSource()); + + // Opening the listener establishes the control channel to + // the Azure Relay service. The control channel is continuously + // maintained, and is reestablished when connectivity is disrupted. + await HttpRelayListener.OpenAsync(ListenerRequestHandler); + Console.WriteLine("Http Server listening"); + + // Start a new thread that will continuously read messages over Http. + await HttpRelayListener.ListenAsync(); + } + catch (Exception e) + { + await Console.Error.WriteLineAsync(e.Message); + throw; + } + + return true; + } + + + + /// + /// Listener Response Handler + /// + /// + static async void ListenerRequestHandler(RelayedHttpListenerContext context) + { + var startTimeUtc = DateTime.UtcNow; + + try + { + // We'll use the Websocket client to send the content to a Websocket connection listener + // Create the Websocket client + WebsocketClient = new WebsocketClient( + RelayNamespace, + RelayConnectionName, + RelayKeyName, + RelayPolicyName); + + // Initiate the connection + var relayConnection = await WebsocketClient.CreateConnectionAsync(); + if (!relayConnection) + { + // There is no websocket listener that is actively listening to our connections, let's try again later + return; + } + + // Listen to messages on the websocket connection + var messageSent = await WebsocketClient.RelayAsync(context); + + await Logger.OutputRequestAsync(messageSent); + + // Close Websocket connection + await WebsocketClient.CloseConnectionAsync(); + } + catch (Exception ex) + { + Logger.LogException(ex); + HttpListener.SendErrorResponse(ex, context); + } + finally + { + Logger.LogPerformanceMetrics(startTimeUtc); + // The context MUST be closed here + await context.Response.CloseAsync(); + } + } + + /// + /// Outputs the Http listener's event messages + /// + /// + static void HttpEventHandler(string eventMessage) + { + Logger.LogRequest("Http", ListenerTargetAddress, $"\u001b[36m {System.Net.HttpStatusCode.OK} \u001b[0m", eventMessage); + Console.WriteLine(eventMessage); + } + + + } +} diff --git a/src/Microsoft.HybridConnections.Relay/WebSocketClient.csproj b/src/Microsoft.HybridConnections.Relay/WebSocketClient.csproj new file mode 100644 index 0000000..0f14913 --- /dev/null +++ b/src/Microsoft.HybridConnections.Relay/WebSocketClient.csproj @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Microsoft.HybridConnections.Relay/WebsocketClient.cs b/src/Microsoft.HybridConnections.Relay/WebsocketClient.cs new file mode 100644 index 0000000..15ac014 --- /dev/null +++ b/src/Microsoft.HybridConnections.Relay/WebsocketClient.cs @@ -0,0 +1,101 @@ +using Microsoft.Azure.Relay; +using Microsoft.HybridConnections.Core; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.HybridConnections.Relay +{ + public class WebsocketClient + { + private readonly HybridConnectionClient _client; + private HybridConnectionStream _relayConnection; + + /// + /// Constructor + /// + /// + /// + /// + /// + public WebsocketClient(string relayNamespace, string connectionName, string keyName, string key) + { + // Create a new hybrid connection client + var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(keyName, key); + _client = new HybridConnectionClient(new Uri($"sb://{relayNamespace}/{connectionName}"), tokenProvider); + } + + /// + /// Initiate the websocket connection + /// + /// + public async Task CreateConnectionAsync() + { + try + { + _relayConnection = await _client.CreateConnectionAsync(); + return true; + } + catch (EndpointNotFoundException) + { + Logger.LogRequest("WebSocket", _client.Address.LocalPath, "\u001b[30m Failed \u001b[0m", $"There are no listeners connected for this endpoint: {_client.Address.AbsoluteUri}."); + return false; + } + catch (Exception e) + { + Logger.LogException(e); + return false; + } + } + + /// + /// Send buffer to the websocket listener + /// + /// + /// + public async Task SendAsync(string buffer) + { + using (var writer = new StreamWriter(_relayConnection) { AutoFlush = true }) + { + await writer.WriteAsync(buffer); + } + } + + /// + /// Relay the RelayedHttpListenerContext to the Websocket Listener + /// + /// + /// + public async Task RelayAsync(RelayedHttpListenerContext context) + { + try + { + var requestMessageSer = await RelayedHttpListenerRequestSerializer.SerializeAsync(context.Request); + // Send to the websocket listener + await SendAsync(requestMessageSer); + + return requestMessageSer; + } + catch (Exception e) + { + Logger.LogException(e); + throw; + } + } + + /// + /// Close the websocket connection + /// + /// + public async Task CloseConnectionAsync() + { + await _relayConnection.CloseAsync(CancellationToken.None); + } + } +} diff --git a/src/Microsoft.HybridConnections.Relay/appsettings.json.template b/src/Microsoft.HybridConnections.Relay/appsettings.json.template new file mode 100644 index 0000000..c59288f --- /dev/null +++ b/src/Microsoft.HybridConnections.Relay/appsettings.json.template @@ -0,0 +1,19 @@ +{ + "Log": { + "Verbose": true | false + }, + "Listener": { + "Namespace": "", + "ConnectionName": "", + "PolicyName": "", + "PolicyKey": "", + "TargetServiceAddress": "" + }, + "Relay": { + "Namespace": "", + "ConnectionName": "", + "PolicyName": "", + "PolicyKey": "", + "RetryFrequency": 5000 + } +} diff --git a/src/Microsoft.HybridConnections.sln b/src/Microsoft.HybridConnections.sln new file mode 100644 index 0000000..35bd3b5 --- /dev/null +++ b/src/Microsoft.HybridConnections.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31410.357 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.HybridConnections.Relay", "Microsoft.HybridConnections.Relay\Microsoft.HybridConnections.Relay.csproj", "{E9049515-08AE-4B2D-8F2E-DC19D820C865}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.HybridConnections.Core", "Microsoft.HybridConnections.Core\Microsoft.HybridConnections.Core.csproj", "{CA1B223D-4BB7-41B2-AE70-67BA527AB284}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetPassage", "NetPassage\NetPassage.csproj", "{C69711F5-7E40-4222-812A-4A88D3CC5519}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E9049515-08AE-4B2D-8F2E-DC19D820C865}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9049515-08AE-4B2D-8F2E-DC19D820C865}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9049515-08AE-4B2D-8F2E-DC19D820C865}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9049515-08AE-4B2D-8F2E-DC19D820C865}.Release|Any CPU.Build.0 = Release|Any CPU + {CA1B223D-4BB7-41B2-AE70-67BA527AB284}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA1B223D-4BB7-41B2-AE70-67BA527AB284}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA1B223D-4BB7-41B2-AE70-67BA527AB284}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA1B223D-4BB7-41B2-AE70-67BA527AB284}.Release|Any CPU.Build.0 = Release|Any CPU + {C69711F5-7E40-4222-812A-4A88D3CC5519}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C69711F5-7E40-4222-812A-4A88D3CC5519}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C69711F5-7E40-4222-812A-4A88D3CC5519}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C69711F5-7E40-4222-812A-4A88D3CC5519}.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/NetPassage/NetPassage.csproj b/src/NetPassage/NetPassage.csproj new file mode 100644 index 0000000..4c20b39 --- /dev/null +++ b/src/NetPassage/NetPassage.csproj @@ -0,0 +1,37 @@ + + + + Exe + net5.0 + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + + + + + + + + + + + + + + + diff --git a/src/NetPassage/NetPassage.json.template b/src/NetPassage/NetPassage.json.template new file mode 100644 index 0000000..f044c4a --- /dev/null +++ b/src/NetPassage/NetPassage.json.template @@ -0,0 +1,19 @@ +{ + "Relay": { + "Mode": " | " + }, + "Http": { + "Namespace": "", + "ConnectionName": "", + "PolicyName": "", + "PolicyKey": "", + "TargetServiceAddress": "" + }, + "WebSocket": { + "Namespace": "", + "ConnectionName": "", + "PolicyName": "", + "PolicyKey": "", + "TargetServiceAddress": "" + } +} diff --git a/src/NetPassage/Program.cs b/src/NetPassage/Program.cs new file mode 100644 index 0000000..2444c9e --- /dev/null +++ b/src/NetPassage/Program.cs @@ -0,0 +1,352 @@ +using Microsoft.Azure.Relay; +using Microsoft.Extensions.Configuration; +using Microsoft.HybridConnections.Core; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace NetPassage +{ + class Program + { + private static bool KeepRunning = true; + private static IConfiguration AppConfig; + private static IConfiguration UserConfig; + private static string LeftSectionFiller; + private static string MidSectionFiller; + private static string ConnectionName; + private static string TargetHttpRelay; + private static bool IsHttpRelayMode; + private static string ConnectionStatus = "offline"; + + static void Main(string[] args) + { + Console.CancelKeyPress += (sender, eventArgs) => { + // call methods to clean up + eventArgs.Cancel = true; + Program.KeepRunning = false; + }; + + AppConfig = new ConfigurationBuilder() + .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) + .AddJsonFile("appsettings.json", false, true) + .AddEnvironmentVariables() + .Build(); + + ShowHeader(AppConfig); + + // Set format + Logger.MaxRows = int.Parse(AppConfig["App:MessageRows"]); + Logger.LeftPad = int.Parse(AppConfig["App:LeftPad"]); + Logger.MidPad = int.Parse(AppConfig["App:MidPad"]); + LeftSectionFiller = string.Empty.PadRight(Logger.LeftPad, ' '); + MidSectionFiller = string.Empty.PadRight(Logger.MidPad, ' '); + + if (args.Length < 1) + { + ShowError("Missing configuration file"); + Environment.Exit(0); + } + + UserConfig = new ConfigurationBuilder() + .AddJsonFile(args[0], false, true) + .Build(); + + IsHttpRelayMode = UserConfig["Relay:Mode"].Equals("http", StringComparison.CurrentCultureIgnoreCase); + + var configHeader = IsHttpRelayMode ? "Http" : "WebSocket"; + + var relayNamespace = $"{UserConfig[$"{configHeader}:Namespace"]}.servicebus.windows.net"; + var keyName = UserConfig[$"{configHeader}:PolicyName"]; + var key = UserConfig[$"{configHeader}:PolicyKey"]; + + ConnectionName = UserConfig[$"{configHeader}:ConnectionName"]; + TargetHttpRelay = UserConfig[$"{configHeader}:TargetServiceAddress"]; + + try + { + while (Program.KeepRunning) + { + ShowAll(); + // Do your work in here, in small chunks. + // If you literally just want to wait until ctrl-c, + // not doing anything, see the answer using set-reset events. + if (IsHttpRelayMode) + { + // Create the Http hybrid proxy listener + var httpRelayListener = new HttpListener( + relayNamespace, + ConnectionName, + keyName, + key, + TargetHttpRelay, + ConnectionEventHandler, + new CancellationTokenSource()); + + // Opening the listener establishes the control channel to + // the Azure Relay service. The control channel is continuously + // maintained, and is reestablished when connectivity is disrupted. + Program.KeepRunning = RunHttpRelayAsync(httpRelayListener).GetAwaiter().GetResult(); + } + else // WebSockets Relay Mode + { + // Create the WebSockets hybrid proxy listener + var webSocketListener = new WebSocketListener( + relayNamespace, + ConnectionName, + keyName, + key, + ConnectionEventHandler, + new CancellationTokenSource()); + + // Opening the listener establishes the control channel to + // the Azure Relay service. The control channel is continuously + // maintained, and is reestablished when connectivity is disrupted. + Program.KeepRunning = RunWebSocketRelayAsync(webSocketListener).GetAwaiter().GetResult(); + } + } + } + catch (Exception e) + { + ShowError(e.Message); + } + } + + /// + /// RunHttpRelayAsync + /// + /// + /// + static async Task RunHttpRelayAsync(HttpListener httpRelayListener) + { + await httpRelayListener.OpenAsync(ProcessHttpMessagesHandler); + + // Start a new thread that will continuously read the console. + await httpRelayListener.ListenAsync(); + + // Close the connection + await httpRelayListener.CloseAsync(); + + // Return false, if the cancellation was requested, otherwise - true + return !httpRelayListener.CTS.IsCancellationRequested; + } + + /// + /// RunWebsocketRelayAsync + /// + /// + /// + static async Task RunWebSocketRelayAsync(WebSocketListener webSocketListener) + { + // Opening the listener establishes the control channel to + // the Azure Relay service. The control channel is continuously + // maintained, and is reestablished when connectivity is disrupted. + await webSocketListener.OpenAsync(); + + // Start a new thread that will continuously read the from the websocket and write to the target Http endpoint. + await webSocketListener.ListenAsync(ProcessWebSocketMessagesHandler); + + // Close Websocket connection + await webSocketListener.CloseAsync(); + + // Return true, if the cancellation was requested, otherwise - false + return webSocketListener.CTS.IsCancellationRequested; + } + + /// + /// Listener Response Handler + /// + /// + static async void ProcessHttpMessagesHandler(RelayedHttpListenerContext context) + { + var startTimeUtc = DateTime.UtcNow; + + try + { + // Send the request message to the target listener + var requestMessage = await HttpListener.CreateHttpRequestMessageAsync(context, ConnectionName); + var responseMessage = await SendHttpRequestAsync(requestMessage); + Logger.LogRequest(requestMessage.Method.Method, requestMessage.RequestUri.LocalPath, $"\u001b[32m {responseMessage.StatusCode} \u001b[0m", $"Forwarded to {TargetHttpRelay}.", ShowAll); + + // Send the response message back to the caller + // await HttpListener.SendResponseAsync(context, responseMessage); + } + catch (RelayException re) + { + Logger.LogRequest("Http", ConnectionName, $"\u001b[31m {System.Net.HttpStatusCode.ServiceUnavailable} \u001b[0m", re.Message, ShowAll); + } + catch (Exception e) + { + Logger.LogRequest("Http", ConnectionName, $"\u001b[31m {e.GetType().Name} \u001b[0m", e.Message, ShowAll); + HttpListener.SendErrorResponse(e, context); + } + finally + { + Logger.LogPerformanceMetrics(startTimeUtc); + // The context MUST be closed here + await context.Response.CloseAsync(); + } + } + + + /// + /// The method initiates the connection. + /// + /// + /// + static async void ProcessWebSocketMessagesHandler(HybridConnectionStream relayConnection, CancellationTokenSource cts) + { + try + { + // The connection is a relay fork. + // We put a stream reader on the input stream and a stream writer over to the target connection + // that allows us to read UTF-8 text data that comes from + // the sender and to write text to the target endpoint. + var reader = new StreamReader(relayConnection); + + // Read a line of input until the end of the buffer + var data = await reader.ReadToEndAsync(); + + Logger.LogRequest("WebSocket", ConnectionName, $"\u001b[36m {System.Net.HttpStatusCode.Redirect} \u001b[0m", $"Received {data.Length} bytes from {relayConnection.TrackingContext.Address}", ShowAll); + + // Deserialize the websocket data into HttpRequestMessage + var requestMessage = RelayedHttpListenerRequestSerializer.Deserialize(data); + + // Send the request message to the target listener + var responseMessage = await SendHttpRequestAsync(requestMessage); + Logger.LogRequest(requestMessage.Method.Method, requestMessage.RequestUri.LocalPath, $"\u001b[32m {responseMessage.StatusCode} \u001b[0m", $"Forwarded to {TargetHttpRelay}.", ShowAll); + } + catch (RelayException re) + { + Logger.LogRequest("WebSocket", ConnectionName, $"\u001b[31m {System.Net.HttpStatusCode.ServiceUnavailable} \u001b[0m", re.Message, ShowAll); + } + catch (Exception e) + { + Logger.LogRequest("WebSocket", ConnectionName, $"\u001b[31m {e.GetType().Name} \u001b[0m", e.Message, ShowAll); + } + finally + { + // If there's no input data, signal that + // you will no longer send data on this connection. + await relayConnection.ShutdownAsync(cts.Token); + + // closing the connection from this end + await relayConnection.CloseAsync(cts.Token); + } + } + + + /// + /// Creates and sends the Stream message over Http Relay connection + /// + /// + /// + static async Task SendHttpRequestAsync(HttpRequestMessage requestMessage) + { + try + { + // Send the request message via Http + using (var httpClient = new HttpClient { BaseAddress = new Uri(TargetHttpRelay, UriKind.RelativeOrAbsolute) }) + { + httpClient.DefaultRequestHeaders.ExpectContinue = false; + return await httpClient.SendAsync(requestMessage); + } + } + catch (Exception e) + { + Console.Error.WriteLine(e); + throw; + } + } + + static void ShowAll() + { + Console.Clear(); + ShowHeader(AppConfig); + ShowConfiguration(UserConfig); + ShowRequestsHeader(); + + List logs = new List(); + logs.AddRange(Logger.Logs); + logs.Reverse(); + + foreach (var message in logs) + { + Console.WriteLine(message); + } + } + + static void ShowHeader(IConfiguration config) + { + var appName = config["App:Name"]; + var author = config["App:Author"]; + var version = config["App:Version"]; + + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write(appName); + Console.ForegroundColor = ConsoleColor.White; + Console.Write(" by "); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine(author); + Console.WriteLine($"Version: {version}"); + Console.ResetColor(); + Console.WriteLine("\n\r\n\r"); + } + + static void ShowError(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.WriteLine(""); + Console.ResetColor(); + } + + static void ShowConfiguration(IConfiguration config) + { + var relayNamespace = $"sb://{config["Relay:Namespace"]}.servicebus.windows.net"; + var IsHttpRelayMode = config["Relay:Mode"].Equals("http", StringComparison.CurrentCultureIgnoreCase); + + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine($"{LeftSectionFiller}{MidSectionFiller}(Ctrl+C to quit)"); + Console.ForegroundColor = ConsoleColor.Green; + var title = "Session Status"; + var filler = string.Empty.PadRight(LeftSectionFiller.Length - title.Length > 0 ? LeftSectionFiller.Length - title.Length : 0); + Console.WriteLine($"{title}{filler}{MidSectionFiller}{ConnectionStatus}"); + + Console.ForegroundColor = ConsoleColor.White; + title = "Azure Relay Namespace"; + filler = string.Empty.PadRight(LeftSectionFiller.Length - title.Length > 0 ? LeftSectionFiller.Length - title.Length : 0); + Console.WriteLine($"{title}{filler}{MidSectionFiller}{relayNamespace}"); + + title = IsHttpRelayMode? "Http Forwarding" : "Websocket Forwarding"; + filler = string.Empty.PadRight(LeftSectionFiller.Length - title.Length > 0 ? LeftSectionFiller.Length - title.Length : 0); + Console.WriteLine($"{title}{filler}{MidSectionFiller}{relayNamespace}/{ConnectionName} {(char)29} {TargetHttpRelay}"); + } + + /// + /// Show Request Logs Header + /// + static void ShowRequestsHeader() + { + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine("\n\r\n\r"); + Console.WriteLine(IsHttpRelayMode ? "HTTP Requests" : "Websocket Requests"); + Console.WriteLine("___________________"); + Console.WriteLine("\n\r"); + } + + + /// + /// Outputs the listener's event messages + /// + /// + static void ConnectionEventHandler(string eventMessage) + { + ConnectionStatus = eventMessage; + ShowAll(); + } + } +} diff --git a/src/NetPassage/appsettings.json.template b/src/NetPassage/appsettings.json.template new file mode 100644 index 0000000..191ecf2 --- /dev/null +++ b/src/NetPassage/appsettings.json.template @@ -0,0 +1,10 @@ +{ + "App": { + "Name": "NetPassage", + "Author": "Microsoft", + "Version": "1.0.0", + "MessageRows": 10, + "LeftPad": 50, + "MidPad": 20 + } +} \ No newline at end of file