├── .gitignore ├── LICENSE ├── README.md └── Src ├── ServiceBusRelayUtil.sln ├── ServiceBusRelayUtil ├── App.config ├── App.ico ├── DispatcherService.cs ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── RawContentTypeMapper.cs ├── ServiceBusRelayUtil.csproj ├── ServiceBusRelayUtilConfig.cs ├── azureservicebusrelaylogo_150.png └── packages.config └── ServiceBusRelayUtilNetCore ├── App.ico ├── DispatcherService.cs ├── Extensions └── StringEx.cs ├── Program.cs ├── ServiceBusRelayUtilNetCore.csproj └── appsettings.json /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Gabo Gilabert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | A relay utility for bots based on Azure Service Bus. 3 | 4 | This utility allows you to forward a message sent to a bot hosted on any channel to your local machine. 5 | 6 | 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). 7 | 8 | ## Acknowledgments 9 | 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). 10 | 11 | # How to configure and run the utility 12 | ### Building with .Net Framework 13 | 14 | 1. Once the solution has been cloned to your machine, open the solution in Visual Studio. 15 | 16 | 2. In Solution Explorer, expand the **ServiceBusRelayUtil** folder. 17 | 18 | 3. Open the **App.config** file and replace the following values with those from your service bus (not the hybrid connection). 19 | 20 | a. "RelayNamespace" is the name of your service bus created earlier. Enter the value in place of **[Your Namespace]**. 21 | 22 | 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]**. 23 | 24 | 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]**. 25 | 26 | 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]**. 27 | 28 | 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]". 29 | 30 | 4. Before testing the relay, your Azure Web Bot's messaging endpoint must be updated to match the relay. 31 | 32 | a. Login to the Azure portal and open your Web App Bot. 33 | 34 | b. Select **Settings** under Bot management to open the settings blade. 35 | 36 | 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. 37 | 38 | 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". 39 | 40 | e. Click **Save** when completed. 41 | 42 | 5. In Visual Studio, press **F5** to run the project. 43 | 44 | 6. Open and run your locally hosted bot. 45 | 46 | 7. Test your bot on a channel (Test in Web Chat, Skype, Teams, etc.). User data is captured and logged as activity occurs. 47 | 48 | - 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**. 49 | 50 | 8. Once testing is completed, you can compile the project into an executable. 51 | 52 | a. Right click the project folder in Visual Studio and select **Build**. 53 | 54 | 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. 55 | - The **app.config** is in the same folder and can be edited as credentials change without needing to recompile the project. 56 | 57 | ### Building with .Net Core 58 | 59 | 1. Once the solution has been cloned to your machine, open the solution in Visual Studio. 60 | 61 | 2. In Solution Explorer, expand the **ServiceBusRelayUtilNetCore** folder. 62 | 63 | 3. Open the **appsettings.json** file and replace the following values with those from your service bus hybrid connection. 64 | 65 | a. "RelayNamespace" is the name of your service bus created earlier. Enter the value in place of **[Your Namespace]**. 66 | 67 | b. "RelayName" is the name of the hybrid connection created in step 12. Enter the value in place of **[Your Relay Name]**. 68 | 69 | 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]**. 70 | 71 | 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]**. 72 | 73 | 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". 74 | 75 | 4. Before testing the relay, your Azure Web App Bot's messaging endpoint must be updated to match the relay. 76 | 77 | a. Login to the Azure portal and open your Web App Bot. 78 | 79 | b. Select **Settings** under Bot management to open the settings blade. 80 | 81 | c. In the **Messaging endpoint** field, enter the service bus namespace and relay. 82 | 83 | 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". 84 | 85 | e. Click **Save** when completed. 86 | 87 | 5. In Visual Studio, press **F5** to run the project. 88 | 89 | 6. Open and run your locally hosted bot. 90 | 91 | 7. Test your bot on a channel (Test in Web Chat, Skype, Teams, etc.). User data is captured and logged as activity occurs. 92 | 93 | - 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**. 94 | 95 | 8. Once testing is completed, you can compile the project into an executable. 96 | 97 | a. Right click the project folder in Visual Studio and select **Publish**. 98 | 99 | b. For **Pick a publish Target**, select **Folder**. 100 | 101 | c. For **Folder or File Share**, choose an output location or keep the default. 102 | 103 | d. Click **Create Profile** to create a publish profile. 104 | 105 | e. Click **Configure...** to change the build configuration and change the following: 106 | 107 | - **Configuration** to "Debug | Any CPU" 108 | - **Deployment Mode** to "Self-contained" 109 | - **Target Runtime** to "win-x64" 110 | 111 | f. Click **Save** and then **Publish** 112 | 113 | 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. 114 | - The **appsettings.json** is in the same folder and can be edited as credentials change without needing to recompile the project. 115 | -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtil.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2005 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceBusRelayUtil", "ServiceBusRelayUtil\ServiceBusRelayUtil.csproj", "{B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceBusRelayUtilNetCore", "ServiceBusRelayUtilNetCore\ServiceBusRelayUtilNetCore.csproj", "{9AECFF0E-26C7-4D96-A00A-8A09198711EC}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {9AECFF0E-26C7-4D96-A00A-8A09198711EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {9AECFF0E-26C7-4D96-A00A-8A09198711EC}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {9AECFF0E-26C7-4D96-A00A-8A09198711EC}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {9AECFF0E-26C7-4D96-A00A-8A09198711EC}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {2C96A03F-F825-402A-B109-0F1FC60BA3CE} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtil/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtil/App.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabog/AzureServiceBusBotRelay/6151ecf933029cd51e86f9969d25566e896c4093/Src/ServiceBusRelayUtil/App.ico -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtil/DispatcherService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.ServiceModel; 7 | using System.ServiceModel.Channels; 8 | using System.ServiceModel.Web; 9 | using System.Text; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using System.Xml; 13 | using Microsoft.ServiceBus.Web; 14 | using Newtonsoft.Json; 15 | using Newtonsoft.Json.Linq; 16 | using Formatting = Newtonsoft.Json.Formatting; 17 | 18 | namespace GaboG.ServiceBusRelayUtil 19 | { 20 | [ServiceContract(Namespace = "http://samples.microsoft.com/ServiceModel/Relay/")] 21 | [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)] 22 | internal class DispatcherService 23 | { 24 | private static readonly HashSet _httpContentHeaders = new HashSet 25 | { 26 | "Allow", 27 | "Content-Encoding", 28 | "Content-Language", 29 | "Content-Length", 30 | "Content-Location", 31 | "Content-MD5", 32 | "Content-Range", 33 | "Content-Type", 34 | "Expires", 35 | "Last-Modified" 36 | }; 37 | 38 | private readonly ServiceBusRelayUtilConfig _config; 39 | 40 | public DispatcherService(ServiceBusRelayUtilConfig config) 41 | { 42 | _config = config; 43 | } 44 | 45 | [WebGet(UriTemplate = "*")] 46 | [OperationContract(AsyncPattern = true)] 47 | public async Task GetAsync() 48 | { 49 | try 50 | { 51 | var ti0 = DateTime.Now; 52 | Console.WriteLine("In GetAsync:"); 53 | var context = WebOperationContext.Current; 54 | var request = BuildForwardedRequest(context, null); 55 | Console.WriteLine("...calling {0}...", request.RequestUri); 56 | HttpResponseMessage response; 57 | using (var client = new HttpClient()) 58 | { 59 | response = await client.SendAsync(request, CancellationToken.None); 60 | } 61 | 62 | Console.WriteLine("...and back {0:N0} ms...", DateTime.Now.Subtract(ti0).TotalMilliseconds); 63 | Console.WriteLine(""); 64 | 65 | Console.WriteLine("...reading and creating response..."); 66 | CopyHttpResponseMessageToOutgoingResponse(response, context.OutgoingResponse); 67 | var stream = response.Content != null ? await response.Content.ReadAsStreamAsync() : null; 68 | var message = StreamMessageHelper.CreateMessage(MessageVersion.None, "GETRESPONSE", stream ?? new MemoryStream()); 69 | Console.WriteLine("...and done (total time: {0:N0} ms).", DateTime.Now.Subtract(ti0).TotalMilliseconds); 70 | Console.WriteLine(""); 71 | return message; 72 | } 73 | catch (Exception ex) 74 | { 75 | WriteException(ex); 76 | throw; 77 | } 78 | } 79 | 80 | [WebInvoke(UriTemplate = "*", Method = "*")] 81 | [OperationContract(AsyncPattern = true)] 82 | public async Task InvokeAsync(Message msg) 83 | { 84 | try 85 | { 86 | var ti0 = DateTime.Now; 87 | WriteFlowerLine(); 88 | Console.WriteLine("In InvokeAsync:"); 89 | var context = WebOperationContext.Current; 90 | var request = BuildForwardedRequest(context, msg); 91 | Console.WriteLine("...calling {0}", request.RequestUri); 92 | HttpResponseMessage response; 93 | using (var client = new HttpClient()) 94 | { 95 | response = await client.SendAsync(request, CancellationToken.None); 96 | } 97 | 98 | Console.WriteLine("...and done {0:N0} ms...", DateTime.Now.Subtract(ti0).TotalMilliseconds); 99 | 100 | Console.WriteLine("...reading and creating response..."); 101 | CopyHttpResponseMessageToOutgoingResponse(response, context.OutgoingResponse); 102 | var stream = response.Content != null ? await response.Content.ReadAsStreamAsync() : null; 103 | var message = StreamMessageHelper.CreateMessage(MessageVersion.None, "GETRESPONSE", stream ?? new MemoryStream()); 104 | Console.WriteLine("...and done (total time: {0:N0} ms).", DateTime.Now.Subtract(ti0).TotalMilliseconds); 105 | return message; 106 | } 107 | catch (Exception ex) 108 | { 109 | WriteException(ex); 110 | throw; 111 | } 112 | } 113 | 114 | private HttpRequestMessage BuildForwardedRequest(WebOperationContext context, Message msg) 115 | { 116 | var incomingRequest = context.IncomingRequest; 117 | 118 | var mappedUri = new Uri(incomingRequest.UriTemplateMatch.RequestUri.ToString().Replace(_config.RelayAddress.ToString(), _config.TargetAddress.ToString())); 119 | var newRequest = new HttpRequestMessage(new HttpMethod(incomingRequest.Method), mappedUri); 120 | 121 | // Copy headers 122 | var hostHeader = _config.TargetAddress.Host + (_config.TargetAddress.Port != 80 || _config.TargetAddress.Port != 443 ? ":" + _config.TargetAddress.Port : ""); 123 | foreach (var name in incomingRequest.Headers.AllKeys.Where(name => !_httpContentHeaders.Contains(name))) 124 | { 125 | newRequest.Headers.TryAddWithoutValidation(name, name == "Host" ? hostHeader : incomingRequest.Headers.Get(name)); 126 | } 127 | 128 | if (msg != null) 129 | { 130 | Stream messageStream = null; 131 | if (msg.Properties.TryGetValue("WebBodyFormatMessageProperty", out var value)) 132 | { 133 | if (value is WebBodyFormatMessageProperty prop && (prop.Format == WebContentFormat.Json || prop.Format == WebContentFormat.Raw)) 134 | { 135 | messageStream = StreamMessageHelper.GetStream(msg); 136 | } 137 | } 138 | else 139 | { 140 | var ms = new MemoryStream(); 141 | using (var xw = XmlDictionaryWriter.CreateTextWriter(ms, Encoding.UTF8, false)) 142 | { 143 | msg.WriteBodyContents(xw); 144 | } 145 | 146 | ms.Seek(0, SeekOrigin.Begin); 147 | messageStream = ms; 148 | } 149 | 150 | if (messageStream != null) 151 | { 152 | if (_config.BufferRequestContent) 153 | { 154 | var ms1 = new MemoryStream(); 155 | messageStream.CopyTo(ms1); 156 | ms1.Seek(0, SeekOrigin.Begin); 157 | newRequest.Content = new StreamContent(ms1); 158 | } 159 | else 160 | { 161 | var ms1 = new MemoryStream(); 162 | messageStream.CopyTo(ms1); 163 | ms1.Seek(0, SeekOrigin.Begin); 164 | 165 | var debugMs = new MemoryStream(); 166 | ms1.CopyTo(debugMs); 167 | debugMs.Seek(0, SeekOrigin.Begin); 168 | 169 | var result = Encoding.UTF8.GetString(debugMs.ToArray()); 170 | WriteJsonObject(result); 171 | 172 | ms1.Seek(0, SeekOrigin.Begin); 173 | newRequest.Content = new StreamContent(ms1); 174 | } 175 | 176 | foreach (var name in incomingRequest.Headers.AllKeys.Where(name => _httpContentHeaders.Contains(name))) 177 | { 178 | newRequest.Content.Headers.TryAddWithoutValidation(name, incomingRequest.Headers.Get(name)); 179 | } 180 | } 181 | } 182 | 183 | return newRequest; 184 | } 185 | 186 | private static void WriteException(Exception ex) 187 | { 188 | Console.ForegroundColor = ConsoleColor.Red; 189 | Console.WriteLine(ex); 190 | Console.WriteLine(""); 191 | Console.ResetColor(); 192 | } 193 | 194 | private static void WriteJsonObject(string result) 195 | { 196 | Console.ForegroundColor = ConsoleColor.Yellow; 197 | 198 | var formatted = result; 199 | if (IsValidJson(result)) 200 | { 201 | var s = new JsonSerializerSettings 202 | { 203 | Formatting = Formatting.Indented 204 | }; 205 | 206 | dynamic o = JsonConvert.DeserializeObject(result); 207 | formatted = JsonConvert.SerializeObject(o, s); 208 | } 209 | 210 | Console.WriteLine(formatted); 211 | Console.ResetColor(); 212 | } 213 | 214 | private static void WriteFlowerLine() 215 | { 216 | Console.ForegroundColor = ConsoleColor.Green; 217 | Console.WriteLine("\r\n=> {0:MM/dd/yyyy hh:mm:ss.fff tt} {1}", DateTime.Now, new string('*', 80)); 218 | Console.ResetColor(); 219 | } 220 | 221 | private static void CopyHttpResponseMessageToOutgoingResponse(HttpResponseMessage response, OutgoingWebResponseContext outgoingResponse) 222 | { 223 | outgoingResponse.StatusCode = response.StatusCode; 224 | outgoingResponse.StatusDescription = response.ReasonPhrase; 225 | if (response.Content == null) 226 | { 227 | outgoingResponse.SuppressEntityBody = true; 228 | } 229 | 230 | foreach (var kvp in response.Headers) 231 | { 232 | foreach (var value in kvp.Value) 233 | { 234 | outgoingResponse.Headers.Add(kvp.Key, value); 235 | } 236 | } 237 | 238 | if (response.Content != null) 239 | { 240 | foreach (var kvp in response.Content.Headers) 241 | { 242 | foreach (var value in kvp.Value) 243 | { 244 | outgoingResponse.Headers.Add(kvp.Key, value); 245 | } 246 | } 247 | } 248 | } 249 | 250 | private static bool IsValidJson(string strInput) 251 | { 252 | strInput = strInput.Trim(); 253 | if ((!strInput.StartsWith("{") || !strInput.EndsWith("}")) && (!strInput.StartsWith("[") || !strInput.EndsWith("]"))) 254 | { 255 | return false; 256 | } 257 | 258 | try 259 | { 260 | JToken.Parse(strInput); 261 | return true; 262 | } 263 | catch //some other exception 264 | { 265 | return false; 266 | } 267 | } 268 | } 269 | } -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtil/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using System.ServiceModel.Channels; 4 | using System.ServiceModel.Web; 5 | using Microsoft.ServiceBus; 6 | 7 | namespace GaboG.ServiceBusRelayUtil 8 | { 9 | internal class Program 10 | { 11 | // https://github.com/pmhsfelix/WebApi.Explorations.ServiceBusRelayHost 12 | // https://docs.microsoft.com/en-us/azure/service-bus-relay/service-bus-relay-rest-tutorial 13 | private static void Main() 14 | { 15 | var relayNamespace = ConfigurationManager.AppSettings["RelayNamespace"]; 16 | var relayAddress = ServiceBusEnvironment.CreateServiceUri("https", relayNamespace, ConfigurationManager.AppSettings["RelayName"]); 17 | 18 | var config = new ServiceBusRelayUtilConfig 19 | { 20 | RelayAddress = relayAddress, 21 | RelayPolicyName = ConfigurationManager.AppSettings["PolicyName"], 22 | RelayPolicyKey = ConfigurationManager.AppSettings["PolicyKey"], 23 | MaxReceivedMessageSize = long.Parse(ConfigurationManager.AppSettings["MaxReceivedMessageSize"]), 24 | TargetAddress = new Uri(ConfigurationManager.AppSettings["TargetServiceAddress"]) 25 | }; 26 | 27 | var host = CreateWebServiceHost(config, relayAddress); 28 | host.Open(); 29 | 30 | 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); 31 | Console.WriteLine(); 32 | Console.WriteLine("Press [Enter] to exit"); 33 | Console.ReadLine(); 34 | 35 | host.Close(); 36 | } 37 | 38 | private static WebServiceHost CreateWebServiceHost(ServiceBusRelayUtilConfig config, Uri address) 39 | { 40 | var host = new WebServiceHost(new DispatcherService(config)); 41 | var binding = GetBinding(config.MaxReceivedMessageSize); 42 | var endpoint = host.AddServiceEndpoint(typeof(DispatcherService), binding, address); 43 | var behavior = GetTransportBehavior(config.RelayPolicyName, config.RelayPolicyKey); 44 | endpoint.Behaviors.Add(behavior); 45 | return host; 46 | } 47 | 48 | private static Binding GetBinding(long maxReceivedMessageSize) 49 | { 50 | var webHttpRelayBinding = new WebHttpRelayBinding(EndToEndWebHttpSecurityMode.None, RelayClientAuthenticationType.None) 51 | { 52 | MaxReceivedMessageSize = maxReceivedMessageSize 53 | }; 54 | var bindingElements = webHttpRelayBinding.CreateBindingElements(); 55 | var webMessageEncodingBindingElement = bindingElements.Find(); 56 | webMessageEncodingBindingElement.ContentTypeMapper = new RawContentTypeMapper(); 57 | return new CustomBinding(bindingElements); 58 | } 59 | 60 | private static TransportClientEndpointBehavior GetTransportBehavior(string keyName, string sharedAccessKey) 61 | { 62 | return new TransportClientEndpointBehavior 63 | { 64 | TokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(keyName, sharedAccessKey) 65 | }; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtil/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ServiceBusRelayUtil")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ServiceBusRelayUtil")] 13 | [assembly: AssemblyCopyright("Copyright © 2017")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b9da41e3-4e0a-41d6-b7d1-64ab017d4fed")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtil/RawContentTypeMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ServiceModel.Channels; 3 | 4 | namespace GaboG.ServiceBusRelayUtil 5 | { 6 | internal class RawContentTypeMapper : WebContentTypeMapper 7 | { 8 | public override WebContentFormat GetMessageFormatForContentType(string contentType) 9 | { 10 | return WebContentFormat.Raw; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtil/ServiceBusRelayUtil.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {B9DA41E3-4E0A-41D6-B7D1-64AB017D4FED} 8 | Exe 9 | GaboG.ServiceBusRelayUtil 10 | ServiceBusRelayUtil 11 | v4.7 12 | 512 13 | true 14 | 15 | 16 | 17 | 18 | AnyCPU 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | AnyCPU 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | App.ico 38 | 39 | 40 | 41 | ..\packages\Microsoft.Azure.Amqp.2.3.7\lib\net45\Microsoft.Azure.Amqp.dll 42 | 43 | 44 | ..\packages\Microsoft.Azure.ServiceBus.3.3.0\lib\net461\Microsoft.Azure.ServiceBus.dll 45 | 46 | 47 | ..\packages\Microsoft.Azure.Services.AppAuthentication.1.0.3\lib\net452\Microsoft.Azure.Services.AppAuthentication.dll 48 | 49 | 50 | ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.19.8\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll 51 | 52 | 53 | ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.19.8\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll 54 | 55 | 56 | ..\packages\Microsoft.IdentityModel.Logging.5.2.2\lib\net451\Microsoft.IdentityModel.Logging.dll 57 | 58 | 59 | ..\packages\Microsoft.IdentityModel.Tokens.5.2.2\lib\net451\Microsoft.IdentityModel.Tokens.dll 60 | 61 | 62 | ..\packages\WindowsAzure.ServiceBus.5.1.0\lib\net46\Microsoft.ServiceBus.dll 63 | 64 | 65 | ..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll 66 | 67 | 68 | 69 | 70 | 71 | 72 | ..\packages\System.Diagnostics.DiagnosticSource.4.4.1\lib\net46\System.Diagnostics.DiagnosticSource.dll 73 | 74 | 75 | ..\packages\System.IdentityModel.Tokens.Jwt.5.2.2\lib\net451\System.IdentityModel.Tokens.Jwt.dll 76 | 77 | 78 | ..\packages\System.IO.4.3.0\lib\net462\System.IO.dll 79 | 80 | 81 | ..\packages\System.Net.WebSockets.4.3.0\lib\net46\System.Net.WebSockets.dll 82 | 83 | 84 | ..\packages\System.Net.WebSockets.Client.4.3.2\lib\net46\System.Net.WebSockets.Client.dll 85 | 86 | 87 | ..\packages\System.Runtime.4.3.0\lib\net462\System.Runtime.dll 88 | 89 | 90 | 91 | ..\packages\System.Runtime.Serialization.Primitives.4.3.0\lib\net46\System.Runtime.Serialization.Primitives.dll 92 | 93 | 94 | ..\packages\System.Runtime.Serialization.Xml.4.3.0\lib\net46\System.Runtime.Serialization.Xml.dll 95 | 96 | 97 | ..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net463\System.Security.Cryptography.Algorithms.dll 98 | 99 | 100 | ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll 101 | 102 | 103 | ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll 104 | 105 | 106 | ..\packages\System.Security.Cryptography.X509Certificates.4.3.2\lib\net461\System.Security.Cryptography.X509Certificates.dll 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | Designer 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 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}. 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtil/ServiceBusRelayUtilConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace GaboG.ServiceBusRelayUtil 4 | { 5 | public class ServiceBusRelayUtilConfig 6 | { 7 | public string RelayPolicyName { get; set; } 8 | public string RelayPolicyKey { get; set; } 9 | public Uri RelayAddress { get; set; } 10 | 11 | public bool BufferRequestContent { get; set; } 12 | public long MaxReceivedMessageSize { get; set; } 13 | public Uri TargetAddress { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtil/azureservicebusrelaylogo_150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabog/AzureServiceBusBotRelay/6151ecf933029cd51e86f9969d25566e896c4093/Src/ServiceBusRelayUtil/azureservicebusrelaylogo_150.png -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtil/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtilNetCore/App.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gabog/AzureServiceBusBotRelay/6151ecf933029cd51e86f9969d25566e896c4093/Src/ServiceBusRelayUtilNetCore/App.ico -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtilNetCore/DispatcherService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using GaboG.ServiceBusRelayUtilNetCore.Extensions; 8 | using Microsoft.Azure.Relay; 9 | using Newtonsoft.Json; 10 | using Newtonsoft.Json.Linq; 11 | 12 | namespace GaboG.ServiceBusRelayUtilNetCore 13 | 14 | { 15 | internal class DispatcherService 16 | { 17 | private readonly HttpClient _httpClient; 18 | private readonly string _hybridConnectionSubPath; 19 | private readonly HybridConnectionListener _listener; 20 | private readonly Uri _targetServiceAddress; 21 | 22 | public DispatcherService(string relayNamespace, string connectionName, string keyName, string key, Uri targetServiceAddress) 23 | { 24 | _targetServiceAddress = targetServiceAddress; 25 | 26 | var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(keyName, key); 27 | _listener = new HybridConnectionListener(new Uri($"sb://{relayNamespace}/{connectionName}"), tokenProvider); 28 | 29 | _httpClient = new HttpClient 30 | { 31 | BaseAddress = targetServiceAddress 32 | }; 33 | _httpClient.DefaultRequestHeaders.ExpectContinue = false; 34 | 35 | _hybridConnectionSubPath = _listener.Address.AbsolutePath.EnsureEndsWith("/"); 36 | } 37 | 38 | public async Task OpenAsync(CancellationToken cancelToken) 39 | { 40 | _listener.RequestHandler = ListenerRequestHandler; 41 | await _listener.OpenAsync(cancelToken); 42 | 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); 43 | Console.WriteLine("Press [Enter] to exit"); 44 | } 45 | 46 | public Task CloseAsync(CancellationToken cancelToken) 47 | { 48 | _httpClient.Dispose(); 49 | return _listener.CloseAsync(cancelToken); 50 | } 51 | 52 | private async void ListenerRequestHandler(RelayedHttpListenerContext context) 53 | { 54 | var startTimeUtc = DateTime.UtcNow; 55 | try 56 | { 57 | Console.WriteLine("Calling {0}...", _targetServiceAddress); 58 | var requestMessage = CreateHttpRequestMessage(context); 59 | var responseMessage = await _httpClient.SendAsync(requestMessage); 60 | await SendResponseAsync(context, responseMessage); 61 | await context.Response.CloseAsync(); 62 | } 63 | 64 | catch (Exception ex) 65 | { 66 | LogException(ex); 67 | SendErrorResponse(ex, context); 68 | } 69 | finally 70 | { 71 | LogRequest(startTimeUtc); 72 | } 73 | } 74 | 75 | private async Task SendResponseAsync(RelayedHttpListenerContext context, HttpResponseMessage responseMessage) 76 | { 77 | context.Response.StatusCode = responseMessage.StatusCode; 78 | context.Response.StatusDescription = responseMessage.ReasonPhrase; 79 | foreach (var header in responseMessage.Headers) 80 | { 81 | if (string.Equals(header.Key, "Transfer-Encoding")) 82 | { 83 | continue; 84 | } 85 | 86 | context.Response.Headers.Add(header.Key, string.Join(",", header.Value)); 87 | } 88 | 89 | var responseStream = await responseMessage.Content.ReadAsStreamAsync(); 90 | await responseStream.CopyToAsync(context.Response.OutputStream); 91 | } 92 | 93 | private void SendErrorResponse(Exception ex, RelayedHttpListenerContext context) 94 | { 95 | context.Response.StatusCode = HttpStatusCode.InternalServerError; 96 | context.Response.StatusDescription = $"Internal Server Error: {ex.GetType().FullName}: {ex.Message}"; 97 | context.Response.Close(); 98 | } 99 | 100 | private HttpRequestMessage CreateHttpRequestMessage(RelayedHttpListenerContext context) 101 | { 102 | var requestMessage = new HttpRequestMessage(); 103 | if (context.Request.HasEntityBody) 104 | { 105 | requestMessage.Content = new StreamContent(context.Request.InputStream); 106 | // Experiment to see if I can capture the return message instead of having the bot responding directly (so far it doesn't work). 107 | //var contentStream = new MemoryStream(); 108 | //var writer = new StreamWriter(contentStream); 109 | //var newActivity = requestMessage.Content.ReadAsStringAsync().Result.Replace("https://directline.botframework.com/", "https://localhost:44372/"); 110 | //writer.Write(newActivity); 111 | //writer.Flush(); 112 | //contentStream.Position = 0; 113 | //requestMessage.Content = new StreamContent(contentStream); 114 | var contentType = context.Request.Headers[HttpRequestHeader.ContentType]; 115 | if (!string.IsNullOrEmpty(contentType)) 116 | { 117 | requestMessage.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); 118 | } 119 | } 120 | 121 | var relativePath = context.Request.Url.GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped); 122 | relativePath = relativePath.Replace(_hybridConnectionSubPath, string.Empty, StringComparison.OrdinalIgnoreCase); 123 | requestMessage.RequestUri = new Uri(relativePath, UriKind.RelativeOrAbsolute); 124 | requestMessage.Method = new HttpMethod(context.Request.HttpMethod); 125 | 126 | foreach (var headerName in context.Request.Headers.AllKeys) 127 | { 128 | if (string.Equals(headerName, "Host", StringComparison.OrdinalIgnoreCase) || 129 | string.Equals(headerName, "Content-Type", StringComparison.OrdinalIgnoreCase)) 130 | { 131 | // Don't flow these headers here 132 | continue; 133 | } 134 | 135 | requestMessage.Headers.Add(headerName, context.Request.Headers[headerName]); 136 | } 137 | 138 | LogRequestActivity(requestMessage); 139 | 140 | return requestMessage; 141 | } 142 | 143 | private void LogRequest(DateTime startTimeUtc) 144 | { 145 | var stopTimeUtc = DateTime.UtcNow; 146 | //var buffer = new StringBuilder(); 147 | //buffer.Append($"{startTimeUtc.ToString("s", CultureInfo.InvariantCulture)}, "); 148 | //buffer.Append($"\"{context.Request.HttpMethod} {context.Request.Url.GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped)}\", "); 149 | //buffer.Append($"{(int)context.Response.StatusCode}, "); 150 | //buffer.Append($"{(int)stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds}"); 151 | //Console.WriteLine(buffer); 152 | 153 | Console.WriteLine("...and back {0:N0} ms...", stopTimeUtc.Subtract(startTimeUtc).TotalMilliseconds); 154 | Console.WriteLine(""); 155 | } 156 | 157 | private void LogRequestActivity(HttpRequestMessage requestMessage) 158 | { 159 | var content = requestMessage.Content.ReadAsStringAsync().Result; 160 | Console.ForegroundColor = ConsoleColor.Yellow; 161 | 162 | var formatted = content; 163 | if (IsValidJson(formatted)) 164 | { 165 | var s = new JsonSerializerSettings 166 | { 167 | Formatting = Formatting.Indented 168 | }; 169 | 170 | dynamic o = JsonConvert.DeserializeObject(content); 171 | formatted = JsonConvert.SerializeObject(o, s); 172 | } 173 | 174 | Console.WriteLine(formatted); 175 | Console.ResetColor(); 176 | } 177 | 178 | private static void LogException(Exception ex) 179 | { 180 | Console.ForegroundColor = ConsoleColor.Red; 181 | Console.WriteLine(ex); 182 | Console.WriteLine(""); 183 | Console.ResetColor(); 184 | } 185 | 186 | private static bool IsValidJson(string strInput) 187 | { 188 | strInput = strInput.Trim(); 189 | if ((!strInput.StartsWith("{") || !strInput.EndsWith("}")) && (!strInput.StartsWith("[") || !strInput.EndsWith("]"))) 190 | { 191 | return false; 192 | } 193 | 194 | try 195 | { 196 | JToken.Parse(strInput); 197 | return true; 198 | } 199 | catch 200 | { 201 | return false; 202 | } 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtilNetCore/Extensions/StringEx.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Microsoft. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | 6 | namespace GaboG.ServiceBusRelayUtilNetCore.Extensions 7 | { 8 | public static class StringEx 9 | { 10 | /// 11 | /// Ensures the given string ends with the requested pattern. If it does no allocations are performed. 12 | /// 13 | public static string EnsureEndsWith(this string s, string value, StringComparison comparisonType = StringComparison.Ordinal) 14 | { 15 | if (!string.IsNullOrEmpty(s) && s.EndsWith(value, comparisonType)) 16 | { 17 | return s; 18 | } 19 | 20 | return s + value; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtilNetCore/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Configuration; 6 | 7 | // https://docs.microsoft.com/en-us/azure/service-bus-relay/service-bus-relay-rest-tutorial 8 | // https://github.com/Azure/azure-relay-dotnet 9 | // https://docs.microsoft.com/en-us/azure/service-bus-relay/relay-hybrid-connections-http-requests-dotnet-get-started 10 | 11 | // This is what I think I need 12 | // https://github.com/Azure/azure-relay/blob/master/samples/hybrid-connections/dotnet/hcreverseproxy/README.md 13 | // https://github.com/Azure/azure-relay/tree/master/samples/hybrid-connections/dotnet/hcreverseproxy 14 | 15 | // Publish 16 | // https://stackoverflow.com/questions/44074121/build-net-core-console-application-to-output-an-exe 17 | // https://docs.microsoft.com/en-us/dotnet/core/rid-catalog 18 | 19 | namespace GaboG.ServiceBusRelayUtilNetCore 20 | { 21 | public class Program 22 | { 23 | public static IConfiguration Configuration { get; set; } 24 | 25 | public static void Main(string[] args) 26 | { 27 | var builder = new ConfigurationBuilder() 28 | .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) 29 | .AddJsonFile("appsettings.json", true, true) 30 | .AddEnvironmentVariables(); 31 | Configuration = builder.Build(); 32 | 33 | RunAsync().GetAwaiter().GetResult(); 34 | } 35 | 36 | static async Task RunAsync() 37 | { 38 | var relayNamespace = Configuration["RelayNamespace"]; 39 | var connectionName = Configuration["RelayName"]; 40 | var keyName = Configuration["PolicyName"]; 41 | var key = Configuration["PolicyKey"]; 42 | var targetServiceAddress = new Uri(Configuration["TargetServiceAddress"]); 43 | 44 | var hybridProxy = new DispatcherService(relayNamespace, connectionName, keyName, key, targetServiceAddress); 45 | 46 | await hybridProxy.OpenAsync(CancellationToken.None); 47 | 48 | Console.ReadLine(); 49 | 50 | await hybridProxy.CloseAsync(CancellationToken.None); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtilNetCore/ServiceBusRelayUtilNetCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.1 6 | App.ico 7 | GaboG.ServiceBusRelayUtilNetCore 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Always 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Src/ServiceBusRelayUtilNetCore/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "RelayNamespace": "[Your Namespace].servicebus.windows.net", 3 | "RelayName": "[Your Relay Name]", 4 | "PolicyName": "[Your Shared Access Policy Name]", 5 | "PolicyKey": "[Your Policy's Key]", 6 | "TargetServiceAddress": "http://localhost:[PORT]" 7 | } 8 | --------------------------------------------------------------------------------