├── .github └── workflows │ └── commit-lint.yml ├── .gitignore ├── .whitesource ├── LICENSE ├── README.md ├── commitlint.config.js ├── icon.png ├── src ├── LICENSE.txt ├── WART-Client │ ├── Program.cs │ ├── WART-Client.csproj │ ├── WartTestClient.cs │ ├── WartTestClientCookie.cs │ ├── WartTestClientJwt.cs │ └── appsettings.json ├── WART-Core │ ├── Authentication │ │ ├── Cookie │ │ │ ├── CookieApplicationBuilderExtension.cs │ │ │ └── CookieServiceCollectionExtension.cs │ │ └── JWT │ │ │ ├── JwtApplicationBuilderExtension.cs │ │ │ └── JwtServiceCollectionExtension.cs │ ├── Controllers │ │ ├── WartBaseController.cs │ │ ├── WartController.cs │ │ ├── WartControllerCookie.cs │ │ └── WartControllerJwt.cs │ ├── Entity │ │ ├── WartEvent.cs │ │ └── WartEventWithFilters.cs │ ├── Enum │ │ └── HubType.cs │ ├── Filters │ │ ├── ExcludeWartAttribute.cs │ │ └── GroupWartAttribute.cs │ ├── Helpers │ │ └── SerializationHelper.cs │ ├── Hubs │ │ ├── WartHub.cs │ │ ├── WartHubBase.cs │ │ ├── WartHubCookie.cs │ │ └── WartHubJwt.cs │ ├── Middleware │ │ ├── WartApplicationBuilderExtension.cs │ │ └── WartServiceCollectionExtension.cs │ ├── Serialization │ │ └── JsonArrayOrObjectStringConverter.cs │ ├── Services │ │ ├── WartEventQueueService.cs │ │ └── WartEventWorker.cs │ └── WART-Core.csproj ├── WART-Tests │ ├── Entity │ │ └── WartEventTests.cs │ ├── Middleware │ │ ├── WartApplicationBuilderExtensionTests.cs │ │ └── WartServiceCollectionExtensionTests.cs │ └── WART-Tests.csproj ├── WART-WebApiRealTime.sln └── WART-WebApiRealTime │ ├── Controllers │ ├── TestController.cs │ ├── TestCookieController.cs │ └── TestJwtController.cs │ ├── Entity │ └── TestEntity.cs │ ├── Program.cs │ ├── Startup.cs │ ├── WART-Api.csproj │ ├── appsettings.Development.json │ └── appsettings.json └── wart_logo.jpg /.github/workflows/commit-lint.yml: -------------------------------------------------------------------------------- 1 | name: Commit Lint 2 | permissions: 3 | contents: read 4 | pull-requests: write 5 | 6 | on: 7 | pull_request: 8 | branches: 9 | - main 10 | - develop 11 | push: 12 | branches: 13 | - main 14 | - develop 15 | 16 | jobs: 17 | commit-lint: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | 23 | - name: Run Commit Lint 24 | uses: wagoid/commitlint-github-action@v5 25 | with: 26 | configFile: commitlint.config.js 27 | -------------------------------------------------------------------------------- /.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/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | .DS_Store 332 | src/.DS_Store 333 | src/WART-Core/.DS_Store 334 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff" 8 | }, 9 | "issueSettings": { 10 | "minSeverityLevel": "LOW" 11 | } 12 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Francesco Del Re 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 | # WART - WebApi Real Time 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![Nuget](https://img.shields.io/nuget/v/WART-Core?style=plastic)](https://www.nuget.org/packages/WART-Core) 5 | ![NuGet Downloads](https://img.shields.io/nuget/dt/WART-Core) 6 | [![issues - wart](https://img.shields.io/github/issues/engineering87/WART)](https://github.com/engineering87/WART/issues) 7 | [![stars - wart](https://img.shields.io/github/stars/engineering87/WART?style=social)](https://github.com/engineering87/WART) 8 | 9 | 10 | 11 | WART is a C# .NET library that enables you to extend any Web API controller and forward incoming calls directly to a SignalR hub. This hub then broadcasts notifications containing detailed information about the calls, including both the request and the response. Additionally, WART supports JWT authentication for secure communication with SignalR. 12 | 13 | ## Features 14 | - Converts REST API calls into SignalR events, enabling real-time communication. 15 | - Provides controllers (`WartController`, `WartControllerJwt`, `WartControllerCookie`) for automatic SignalR event broadcasting. 16 | - Supports JWT authentication for SignalR hub connections. 17 | - Allows API exclusion from event broadcasting with `[ExcludeWart]` attribute. 18 | - Enables group-specific event dispatching with `[GroupWart("group_name")]`. 19 | - Configurable middleware (`AddWartMiddleware`) for flexible integration. 20 | 21 | ## Installation 22 | You can install the library via the NuGet package manager with the following command: 23 | 24 | ```bash 25 | dotnet add package WART-Core 26 | ``` 27 | 28 | ### How it works 29 | WART implements a custom controller which overrides the `OnActionExecuting` and `OnActionExecuted` methods to retrieve the request and the response and encapsulates them in a **WartEvent** object which will be sent via SignalR on the **WartHub**. 30 | 31 | ### How to use it 32 | 33 | To use the WART library, each WebApi controller must extend the **WartController** controller: 34 | 35 | ```csharp 36 | using WART_Core.Controllers; 37 | using WART_Core.Hubs; 38 | 39 | [ApiController] 40 | [Route("api/[controller]")] 41 | public class TestController : WartController 42 | ``` 43 | 44 | each controller must implement the following constructor, for example: 45 | 46 | ```csharp 47 | public TestController(IHubContext messageHubContext, 48 | ILogger logger) : base(messageHubContext, logger) 49 | { 50 | } 51 | ``` 52 | 53 | WART support JWT bearer authentication on SignalR hub, if you want to use JWT authentication use the following controller extension: 54 | 55 | ```csharp 56 | using WART_Core.Controllers; 57 | using WART_Core.Hubs; 58 | 59 | [ApiController] 60 | [Route("api/[controller]")] 61 | public class TestController : WartControllerJwt 62 | ``` 63 | 64 | You also need to enable SignalR in the WebAPI solution and map the **WartHub**. 65 | To do this, add the following configurations in the Startup.cs class: 66 | 67 | ```csharp 68 | using WART_Core.Middleware; 69 | ``` 70 | 71 | In the ConfigureServices section add following: 72 | 73 | ```csharp 74 | services.AddWartMiddleware(); 75 | ``` 76 | 77 | or by specifying JWT authentication: 78 | 79 | 80 | ```csharp 81 | services.AddWartMiddleware(hubType:HubType.JwtAuthentication, tokenKey:"password_here"); 82 | ``` 83 | 84 | In the Configure section add the following: 85 | 86 | ```csharp 87 | app.UseWartMiddleware(); 88 | ``` 89 | 90 | or by specifying JWT authentication: 91 | 92 | ```csharp 93 | app.UseWartMiddleware(HubType.JwtAuthentication); 94 | ``` 95 | 96 | Alternatively, it is possible to specify a custom hub name: 97 | 98 | ```csharp 99 | app.UseWartMiddleware("hubname"); 100 | ``` 101 | 102 | at this point it will be sufficient to connect via SignalR to the WartHub to receive notifications in real time of any call on the controller endpoints. 103 | For example: 104 | 105 | ```csharp 106 | var hubConnection = new HubConnectionBuilder() 107 | .WithUrl("http://localhost:52086/warthub") 108 | .Build(); 109 | 110 | hubConnection.On("Send", (data) => 111 | { 112 | // data is the WartEvent JSON 113 | }); 114 | ``` 115 | 116 | or with JWT authentication: 117 | 118 | ```csharp 119 | var hubConnection = new HubConnectionBuilder() 120 | .WithUrl($"http://localhost:51392/warthub", options => 121 | { 122 | options.SkipNegotiation = true; 123 | options.Transports = HttpTransportType.WebSockets; 124 | options.AccessTokenProvider = () => Task.FromResult(GenerateToken()); 125 | }) 126 | .WithAutomaticReconnect() 127 | .Build(); 128 | 129 | hubConnection.On("Send", (data) => 130 | { 131 | // data is the WartEvent JSON 132 | }); 133 | ``` 134 | 135 | In the source code you can find a simple test client and WebApi project. 136 | 137 | ## Supported Authentication Modes 138 | 139 | The project supports three authentication modes for accessing the SignalR Hub: 140 | 141 | | Mode | Description | Hub Class | Required Middleware | 142 | |--------------------------|---------------------------------------------------------------------------|----------------------|---------------------------| 143 | | **No Authentication** | Open access without identity verification | `WartHub` | None | 144 | | **JWT (Bearer Token)** | Authentication via JWT token in the `Authorization: Bearer ` header | `WartHubJwt` | `UseJwtMiddleware()` | 145 | | **Cookie Authentication**| Authentication via HTTP cookies issued after login | `WartHubCookie` | `UseCookieMiddleware()` | 146 | 147 | > ⚙️ Authentication mode is selected through the `HubType` configuration in the application startup. 148 | 149 | ### Excluding APIs from Event Propagation 150 | There might be scenarios where you want to exclude specific APIs from propagating events to connected clients. This can be particularly useful when certain endpoints should not trigger updates, notifications, or other real-time messages through SignalR. To achieve this, you can use a custom filter called `ExcludeWartAttribute`. By decorating the desired API endpoints with this attribute, you can prevent them from being included in the SignalR event propagation logic, for example: 151 | 152 | ```csharp 153 | [HttpGet("{id}")] 154 | [ExcludeWart] 155 | public ActionResult Get(int id) 156 | { 157 | var item = Items.FirstOrDefault(x => x.Id == id); 158 | if (item == null) 159 | { 160 | return NotFound(); 161 | } 162 | return item; 163 | } 164 | ``` 165 | 166 | ### SignalR Event Dispatching for Specific Groups 167 | WART enables sending API events to specific groups in SignalR by specifying the group name in the query string. This approach allows for flexible and targeted event broadcasting, ensuring that only the intended group of clients receives the event. 168 | By decorating an API method with `[GroupWart("group_name")]`, it is possible to specify the SignalR group name to which the dispatch of specific events for that API is restricted. This ensures that only the clients subscribed to the specified group ("SampleGroupName") will receive the related events, allowing for targeted, group-based communication in a SignalR environment. 169 | 170 | ```csharp 171 | [HttpPost] 172 | [GroupWart("SampleGroupName")] 173 | public ActionResult Post([FromBody] TestEntity entity) 174 | { 175 | Items.Add(entity); 176 | return entity; 177 | } 178 | ``` 179 | 180 | By appending `?WartGroup=group_name` to the URL, the library enables dispatching events from individual APIs to a specific SignalR group, identified by `group_name`. This allows for granular control over which clients receive the event, leveraging SignalR’s built-in group functionality. 181 | 182 | ### NuGet 183 | 184 | The library is available on NuGet packetmanager. 185 | 186 | https://www.nuget.org/packages/WART-Core/ 187 | 188 | ### Contributing 189 | Thank you for considering to help out with the source code! 190 | If you'd like to contribute, please fork, fix, commit and send a pull request for the maintainers to review and merge into the main code base. 191 | 192 | **Getting started with Git and GitHub** 193 | 194 | * [Setting up Git](https://docs.github.com/en/get-started/getting-started-with-git/set-up-git) 195 | * [Fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) 196 | * [Open an issue](https://github.com/engineering87/WART/issues) if you encounter a bug or have a suggestion for improvements/features 197 | 198 | ### Licensee 199 | WART source code is available under MIT License, see license in the source. 200 | 201 | ### Contact 202 | Please contact at francesco.delre[at]protonmail.com for any details. 203 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineering87/WART/7f161fd42817aba5d706630c1e6dcd16fe885ae5/icon.png -------------------------------------------------------------------------------- /src/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Francesco Del Re 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. -------------------------------------------------------------------------------- /src/WART-Client/Program.cs: -------------------------------------------------------------------------------- 1 | // (c) 2019 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using System; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Configuration; 6 | 7 | namespace WART_Client 8 | { 9 | public class Program 10 | { 11 | private static async Task Main() 12 | { 13 | Console.WriteLine("Starting WartTestClient"); 14 | 15 | var configuration = new ConfigurationBuilder() 16 | .AddJsonFile("appsettings.json") 17 | .Build(); 18 | 19 | var wartHubUrl = $"{configuration["Scheme"]}://{configuration["Host"]}:{configuration["Port"]}/{configuration["Hubname"]}"; 20 | var wartHubUrlGroup = configuration["WartGroup"] != string.Empty ? $"?WartGroup={configuration["WartGroup"]}" : string.Empty; 21 | wartHubUrl += wartHubUrlGroup; 22 | 23 | Console.WriteLine($"Connecting to {wartHubUrl}"); 24 | 25 | var auth = configuration["AuthenticationType"] ?? "NoAuth"; 26 | 27 | switch (auth.ToLowerInvariant()) 28 | { 29 | default: 30 | case "noauth": 31 | { 32 | await WartTestClient.ConnectAsync(wartHubUrl); 33 | break; 34 | } 35 | case "jwt": 36 | { 37 | var key = configuration["Key"]; 38 | await WartTestClientJwt.ConnectAsync(wartHubUrl, key); 39 | break; 40 | } 41 | case "cookie": 42 | { 43 | await WartTestClientCookie.ConnectAsync(wartHubUrl); 44 | break; 45 | } 46 | } 47 | 48 | Console.WriteLine($"Connected to {wartHubUrl}"); 49 | Console.ReadLine(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/WART-Client/WART-Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net9.0 6 | WART_Client 7 | WART_Client.Program 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | true 19 | PreserveNewest 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/WART-Client/WartTestClient.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Http.Connections; 4 | using Microsoft.AspNetCore.SignalR.Client; 5 | using System; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace WART_Client 10 | { 11 | /// 12 | /// A simple SignalR WART test client without authentication. 13 | /// 14 | public class WartTestClient 15 | { 16 | public static async Task ConnectAsync(string wartHubUrl) 17 | { 18 | try 19 | { 20 | var hubConnection = new HubConnectionBuilder() 21 | .WithUrl(wartHubUrl, options => 22 | { 23 | options.Transports = HttpTransportType.WebSockets | 24 | HttpTransportType.ServerSentEvents | 25 | HttpTransportType.LongPolling; 26 | }) 27 | .WithAutomaticReconnect() 28 | .Build(); 29 | 30 | hubConnection.On("Send", (data) => 31 | { 32 | Console.WriteLine(data); 33 | Console.WriteLine($"Message size: {Encoding.UTF8.GetBytes(data).Length} byte"); 34 | Console.WriteLine(Environment.NewLine); 35 | }); 36 | 37 | hubConnection.Closed += async (exception) => 38 | { 39 | Console.WriteLine(exception); 40 | Console.WriteLine(Environment.NewLine); 41 | await Task.Delay(new Random().Next(0, 5) * 1000); 42 | await hubConnection.StartAsync(); 43 | }; 44 | 45 | hubConnection.On("ConnectionFailed", (exception) => 46 | { 47 | Console.WriteLine(exception); 48 | Console.WriteLine(Environment.NewLine); 49 | return Task.CompletedTask; 50 | }); 51 | 52 | await hubConnection.StartAsync(); 53 | } 54 | catch (Exception e) 55 | { 56 | Console.WriteLine(e.Message); 57 | } 58 | 59 | await Task.CompletedTask; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/WART-Client/WartTestClientCookie.cs: -------------------------------------------------------------------------------- 1 | // (c) 2025 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Http.Connections; 4 | using Microsoft.AspNetCore.SignalR.Client; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace WART_Client 13 | { 14 | /// 15 | /// A simple SignalR WART test client with Cookie authentication. 16 | /// 17 | public static class WartTestClientCookie 18 | { 19 | public static async Task ConnectAsync(string hubUrl) 20 | { 21 | try 22 | { 23 | var cookieContainer = new CookieContainer(); 24 | var handler = new HttpClientHandler 25 | { 26 | CookieContainer = cookieContainer, 27 | UseCookies = true, 28 | AllowAutoRedirect = true 29 | }; 30 | 31 | using var httpClient = new HttpClient(handler); 32 | 33 | var loginContent = new FormUrlEncodedContent(new[] 34 | { 35 | new KeyValuePair("username", "test_username"), 36 | new KeyValuePair("password", "test_password") 37 | }); 38 | 39 | var loginUri = new Uri(new Uri(hubUrl), "/api/TestCookie/login"); 40 | var loginResponse = await httpClient.PostAsync(loginUri, loginContent); 41 | loginResponse.EnsureSuccessStatusCode(); 42 | 43 | Console.WriteLine("Login successful. Connecting to SignalR..."); 44 | 45 | //var uri = new Uri(hubUrl); 46 | //cookieContainer.Add(uri, new Cookie("WART.AuthCookie", "sample_value")); 47 | 48 | var hubConnection = new HubConnectionBuilder() 49 | .WithUrl(hubUrl, options => 50 | { 51 | options.HttpMessageHandlerFactory = _ => handler; 52 | options.Transports = HttpTransportType.WebSockets | 53 | HttpTransportType.ServerSentEvents | 54 | HttpTransportType.LongPolling; 55 | }) 56 | .WithAutomaticReconnect() 57 | .Build(); 58 | 59 | hubConnection.On("Send", (data) => 60 | { 61 | Console.WriteLine(data); 62 | Console.WriteLine($"Message size: {Encoding.UTF8.GetBytes(data).Length} bytes"); 63 | Console.WriteLine(); 64 | }); 65 | 66 | hubConnection.Closed += async (ex) => 67 | { 68 | Console.WriteLine($"Connection closed: {ex?.Message}"); 69 | await Task.Delay(new Random().Next(0, 5) * 1000); 70 | if (hubConnection != null) 71 | await hubConnection.StartAsync(); 72 | }; 73 | 74 | hubConnection.On("ConnectionFailed", (ex) => 75 | { 76 | Console.WriteLine($"Connection failed: {ex.Message}"); 77 | return Task.CompletedTask; 78 | }); 79 | 80 | await hubConnection.StartAsync(); 81 | Console.WriteLine("SignalR connection started."); 82 | } 83 | catch (Exception e) 84 | { 85 | Console.WriteLine(e.Message); 86 | } 87 | 88 | await Task.CompletedTask; 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/WART-Client/WartTestClientJwt.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Http.Connections; 4 | using Microsoft.AspNetCore.SignalR.Client; 5 | using Microsoft.IdentityModel.Tokens; 6 | using System; 7 | using System.IdentityModel.Tokens.Jwt; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace WART_Client 12 | { 13 | /// 14 | /// A simple SignalR WART test client with JWT authentication. 15 | /// 16 | public class WartTestClientJwt 17 | { 18 | private static readonly JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler(); 19 | 20 | public static async Task ConnectAsync(string wartHubUrl, string key) 21 | { 22 | try 23 | { 24 | var hubConnection = new HubConnectionBuilder() 25 | .WithUrl(wartHubUrl, options => 26 | { 27 | options.AccessTokenProvider = () => Task.FromResult(GenerateToken(key)); 28 | options.Transports = HttpTransportType.WebSockets | 29 | HttpTransportType.ServerSentEvents | 30 | HttpTransportType.LongPolling; 31 | }) 32 | .WithAutomaticReconnect() 33 | .Build(); 34 | 35 | hubConnection.On("Send", (data) => 36 | { 37 | Console.WriteLine(data); 38 | Console.WriteLine($"Message size: {Encoding.UTF8.GetBytes(data).Length} byte"); 39 | Console.WriteLine(Environment.NewLine); 40 | }); 41 | 42 | hubConnection.Closed += async (exception) => 43 | { 44 | Console.WriteLine(exception); 45 | Console.WriteLine(Environment.NewLine); 46 | await Task.Delay(new Random().Next(0, 5) * 1000); 47 | await hubConnection.StartAsync(); 48 | }; 49 | 50 | hubConnection.On("ConnectionFailed", (exception) => 51 | { 52 | Console.WriteLine(exception); 53 | Console.WriteLine(Environment.NewLine); 54 | return Task.CompletedTask; 55 | }); 56 | 57 | await hubConnection.StartAsync(); 58 | } 59 | catch (Exception e) 60 | { 61 | Console.WriteLine(e.Message); 62 | } 63 | 64 | await Task.CompletedTask; 65 | } 66 | 67 | private static string GenerateToken(string key) 68 | { 69 | var SecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)); 70 | var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256); 71 | var token = new JwtSecurityToken(expires: DateTime.UtcNow.AddHours(24), signingCredentials: credentials); 72 | return JwtTokenHandler.WriteToken(token); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/WART-Client/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Scheme": "https", 3 | "Host": "localhost", 4 | "Port": "54644", 5 | "Hubname": "warthub", 6 | "AuthenticationType": "JWT", 7 | "Key": "dn3341fmcscscwe28419brhwbwgbss4t", 8 | "WartGroup": "SampleGroupName" 9 | } -------------------------------------------------------------------------------- /src/WART-Core/Authentication/Cookie/CookieApplicationBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | // (c) 2025 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Builder; 4 | 5 | namespace WART_Core.Authentication.Cookie 6 | { 7 | public static class CookieApplicationBuilderExtension 8 | { 9 | /// 10 | /// Use Cookie authentication dependency to IApplicationBuilder. 11 | /// 12 | /// The IApplicationBuilder to configure the middleware pipeline. 13 | /// 14 | public static IApplicationBuilder UseCookieMiddleware(this IApplicationBuilder app) 15 | { 16 | app.UseAuthentication(); 17 | app.UseAuthorization(); 18 | 19 | return app; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/WART-Core/Authentication/Cookie/CookieServiceCollectionExtension.cs: -------------------------------------------------------------------------------- 1 | // (c) 2025 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.DataProtection; 8 | using Microsoft.AspNetCore.HttpOverrides; 9 | using Microsoft.AspNetCore.ResponseCompression; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Logging; 12 | using WART_Core.Hubs; 13 | using WART_Core.Services; 14 | 15 | namespace WART_Core.Authentication.Cookie 16 | { 17 | public static class CookieServiceCollectionExtension 18 | { 19 | /// 20 | /// Adds Cookie authentication middleware to the service collection. 21 | /// Configures the authentication parameters, SignalR settings, and response compression. 22 | /// 23 | /// The service collection to add the middleware to. 24 | /// Optional path for the login redirect (default: /Account/Login). 25 | /// Optional path for access denied redirect (default: /Account/Denied). 26 | /// The updated service collection. 27 | public static IServiceCollection AddCookieMiddleware( 28 | this IServiceCollection services, 29 | string loginPath = "/Account/Login", 30 | string accessDeniedPath = "/Account/AccessDenied") 31 | { 32 | // Configure forwarded headers (support for reverse proxy) 33 | services.Configure(options => 34 | { 35 | options.ForwardedHeaders = 36 | ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; 37 | }); 38 | 39 | // Add logging support 40 | services.AddLogging(configure => configure.AddConsole()); 41 | 42 | // Add Data Protection with key persistence 43 | var keysPath = Path.Combine(AppContext.BaseDirectory, "keys"); 44 | Directory.CreateDirectory(keysPath); 45 | services.AddDataProtection() 46 | .PersistKeysToFileSystem(new DirectoryInfo(keysPath)) 47 | .SetApplicationName("WART_App"); 48 | 49 | // Configure cookie-based authentication 50 | services.AddAuthentication("WartCookieAuth") 51 | .AddCookie("WartCookieAuth", options => 52 | { 53 | options.LoginPath = loginPath; 54 | options.AccessDeniedPath = accessDeniedPath; 55 | options.Cookie.Name = "WART.AuthCookie"; 56 | options.ExpireTimeSpan = TimeSpan.FromHours(1); 57 | options.SlidingExpiration = true; 58 | }); 59 | 60 | // Register WART event queue service 61 | services.AddSingleton(); 62 | 63 | // Register the WART event worker for the cookie-authenticated hub 64 | services.AddHostedService>(); 65 | 66 | // SignalR configuration 67 | services.AddSignalR(options => 68 | { 69 | options.EnableDetailedErrors = true; 70 | options.HandshakeTimeout = TimeSpan.FromSeconds(15); 71 | options.KeepAliveInterval = TimeSpan.FromSeconds(15); 72 | options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); 73 | }); 74 | 75 | // Compression for SignalR WebSocket/Binary transport 76 | services.AddResponseCompression(opts => 77 | { 78 | opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( 79 | new[] { "application/octet-stream" }); 80 | }); 81 | 82 | return services; 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /src/WART-Core/Authentication/JWT/JwtApplicationBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Builder; 4 | 5 | namespace WART_Core.Authentication.JWT 6 | { 7 | public static class JwtApplicationBuilderExtension 8 | { 9 | /// 10 | /// Use JWT authentication dependency to IApplicationBuilder. 11 | /// 12 | /// The IApplicationBuilder to configure the middleware pipeline. 13 | /// 14 | public static IApplicationBuilder UseJwtMiddleware(this IApplicationBuilder app) 15 | { 16 | app.UseAuthentication(); 17 | app.UseAuthorization(); 18 | 19 | return app; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/WART-Core/Authentication/JWT/JwtServiceCollectionExtension.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using System; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Authentication.JwtBearer; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.HttpOverrides; 9 | using Microsoft.AspNetCore.ResponseCompression; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Logging; 12 | using Microsoft.IdentityModel.Tokens; 13 | using System.Linq; 14 | using WART_Core.Hubs; 15 | using WART_Core.Services; 16 | 17 | namespace WART_Core.Authentication.JWT 18 | { 19 | public static class JwtServiceCollectionExtension 20 | { 21 | /// 22 | /// Adds JWT authentication middleware to the service collection. 23 | /// Configures the authentication parameters, SignalR settings, and response compression. 24 | /// 25 | /// The service collection to add the middleware to. 26 | /// The secret key used to sign and validate the JWT tokens. 27 | /// The updated service collection. 28 | /// Thrown if the token key is null or empty. 29 | public static IServiceCollection AddJwtMiddleware(this IServiceCollection services, string tokenKey) 30 | { 31 | // Validate that the token key is provided 32 | if (string.IsNullOrEmpty(tokenKey)) 33 | { 34 | throw new ArgumentNullException("Invalid token key"); 35 | } 36 | 37 | // Configure forwarded headers (to support proxy scenarios, e.g., when behind a load balancer) 38 | services.Configure(options => 39 | { 40 | options.ForwardedHeaders = 41 | ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; 42 | }); 43 | 44 | // Add logging for debugging purposes 45 | services.AddLogging(configure => configure.AddConsole()); 46 | 47 | // Create a symmetric security key from the provided token key 48 | var key = Encoding.UTF8.GetBytes(tokenKey); 49 | var securityKey = new SymmetricSecurityKey(key); 50 | 51 | // Configure authentication using JWT Bearer token 52 | services.AddAuthentication(options => 53 | { 54 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 55 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 56 | }) 57 | .AddJwtBearer(options => 58 | { 59 | options.IncludeErrorDetails = true; 60 | options.TokenValidationParameters = 61 | new TokenValidationParameters 62 | { 63 | LifetimeValidator = (before, expires, token, parameters) => expires > DateTime.UtcNow, 64 | ValidateAudience = false, 65 | ValidateIssuer = false, 66 | ValidateActor = false, 67 | ValidateLifetime = true, 68 | IssuerSigningKey = securityKey 69 | }; 70 | options.Events = new JwtBearerEvents 71 | { 72 | OnMessageReceived = context => 73 | { 74 | var accessToken = context.Request.Query["access_token"]; 75 | if (!string.IsNullOrEmpty(accessToken)) 76 | { 77 | context.Token = accessToken; 78 | } 79 | return Task.CompletedTask; 80 | } 81 | }; 82 | }); 83 | 84 | // Register WART event queue as a singleton service. 85 | services.AddSingleton(); 86 | 87 | // Register the WART event worker as a hosted service. 88 | services.AddHostedService>(); 89 | 90 | // Configure SignalR options, including error handling and timeouts 91 | services.AddSignalR(options => 92 | { 93 | options.EnableDetailedErrors = true; 94 | options.HandshakeTimeout = TimeSpan.FromSeconds(15); 95 | options.KeepAliveInterval = TimeSpan.FromSeconds(15); 96 | options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); 97 | }); 98 | 99 | // Configure response compression to support additional MIME types 100 | services.AddResponseCompression(opts => 101 | { 102 | opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( 103 | new[] { "application/octet-stream" }); 104 | }); 105 | 106 | return services; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/WART-Core/Controllers/WartBaseController.cs: -------------------------------------------------------------------------------- 1 | // (c) 2024 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.SignalR; 6 | using Microsoft.Extensions.Logging; 7 | using System.Linq; 8 | using WART_Core.Entity; 9 | using WART_Core.Filters; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using WART_Core.Services; 12 | 13 | namespace WART_Core.Controllers 14 | { 15 | public abstract class WartBaseController : Controller where THub : Hub 16 | { 17 | private readonly ILogger _logger; 18 | private readonly IHubContext _hubContext; 19 | private const string RouteDataKey = "REQUEST"; 20 | 21 | private WartEventQueueService _eventQueue; 22 | 23 | protected WartBaseController(IHubContext hubContext, ILogger logger) 24 | { 25 | _hubContext = hubContext; 26 | _logger = logger; 27 | } 28 | 29 | /// 30 | /// Adds the request objects to RouteData. 31 | /// 32 | /// The action executing context. 33 | public override void OnActionExecuting(ActionExecutingContext context) 34 | { 35 | context?.RouteData.Values.Add(RouteDataKey, context.ActionArguments); 36 | base.OnActionExecuting(context); 37 | } 38 | 39 | /// 40 | /// Processes the executed action and sends the event to the SignalR hub if applicable. 41 | /// 42 | /// The action executed context. 43 | public override void OnActionExecuted(ActionExecutedContext context) 44 | { 45 | if (context?.Result is ObjectResult objectResult) 46 | { 47 | var exclusion = context.Filters.Any(f => f.GetType().Name == nameof(ExcludeWartAttribute)); 48 | if (!exclusion && context.RouteData.Values.TryGetValue(RouteDataKey, out var request)) 49 | { 50 | var httpMethod = context.HttpContext?.Request.Method; 51 | var httpPath = context.HttpContext?.Request.Path; 52 | var remoteAddress = context.HttpContext?.Connection.RemoteIpAddress?.ToString(); 53 | var response = objectResult.Value; 54 | 55 | var wartEvent = new WartEvent(request, response, httpMethod, httpPath, remoteAddress); 56 | 57 | _eventQueue = context.HttpContext?.RequestServices.GetService(); 58 | _eventQueue?.Enqueue(new WartEventWithFilters(wartEvent, [.. context.Filters])); 59 | } 60 | } 61 | 62 | base.OnActionExecuted(context); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/WART-Core/Controllers/WartController.cs: -------------------------------------------------------------------------------- 1 | // (c) 2019 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.SignalR; 4 | using Microsoft.Extensions.Logging; 5 | using WART_Core.Hubs; 6 | 7 | namespace WART_Core.Controllers 8 | { 9 | /// 10 | /// The WART Controller 11 | /// 12 | public class WartController : WartBaseController 13 | { 14 | public WartController(IHubContext hubContext, ILogger logger) 15 | : base(hubContext, logger) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/WART-Core/Controllers/WartControllerCookie.cs: -------------------------------------------------------------------------------- 1 | // (c) 2025 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.SignalR; 4 | using Microsoft.Extensions.Logging; 5 | using WART_Core.Hubs; 6 | 7 | namespace WART_Core.Controllers 8 | { 9 | /// 10 | /// The WART Controller with Cookie authentication 11 | /// 12 | public class WartControllerCookie : WartBaseController 13 | { 14 | public WartControllerCookie(IHubContext hubContext, ILogger logger) 15 | : base(hubContext, logger) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/WART-Core/Controllers/WartControllerJwt.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.SignalR; 4 | using Microsoft.Extensions.Logging; 5 | using WART_Core.Hubs; 6 | 7 | namespace WART_Core.Controllers 8 | { 9 | /// 10 | /// The WART Controller with JWT authentication 11 | /// 12 | public class WartControllerJwt : WartBaseController 13 | { 14 | public WartControllerJwt(IHubContext hubContext, ILogger logger) 15 | : base(hubContext, logger) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/WART-Core/Entity/WartEvent.cs: -------------------------------------------------------------------------------- 1 | // (c) 2019 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text.Json.Serialization; 6 | using WART_Core.Helpers; 7 | using WART_Core.Serialization; 8 | 9 | namespace WART_Core.Entity 10 | { 11 | /// 12 | /// Represents an event in the WART system, typically capturing HTTP request and response data, 13 | /// along with additional metadata such as timestamps and remote addresses. 14 | /// This class is serializable and designed to be used for logging or transmitting event data. 15 | /// 16 | [Serializable] 17 | public class WartEvent 18 | { 19 | public Guid EventId { get; set; } 20 | public DateTime TimeStamp { get; set; } 21 | public DateTime UtcTimeStamp { get; set; } 22 | public string HttpMethod { get; set; } 23 | public string HttpPath { get; set; } 24 | public string RemoteAddress { get; set; } 25 | [JsonConverter(typeof(JsonArrayOrObjectStringConverter))] 26 | public string JsonRequestPayload { get; set; } 27 | [JsonConverter(typeof(JsonArrayOrObjectStringConverter))] 28 | public string JsonResponsePayload { get; set; } 29 | public string ExtraInfo { get; set; } 30 | 31 | /// 32 | /// Private constructor used for JSON deserialization. 33 | /// This constructor is necessary for deserializing a `WartEvent` object from JSON. 34 | /// 35 | private WartEvent() { } 36 | 37 | /// 38 | /// Initializes a new instance of the class with the specified HTTP method, path, and remote address. 39 | /// 40 | /// The HTTP method (e.g., GET, POST). 41 | /// The path of the HTTP request. 42 | /// The remote address (IP) from which the request originated. 43 | public WartEvent(string httpMethod, string httpPath, string remoteAddress) 44 | { 45 | this.EventId = Guid.NewGuid(); 46 | this.TimeStamp = DateTime.Now; 47 | this.UtcTimeStamp = DateTime.UtcNow; 48 | this.HttpMethod = httpMethod; 49 | this.HttpPath = httpPath; 50 | this.RemoteAddress = remoteAddress; 51 | } 52 | 53 | /// 54 | /// Initializes a new instance of the class with the specified HTTP method, path, remote address, 55 | /// request payload, and response payload. This constructor is typically used when logging both the request and response data. 56 | /// 57 | /// The request object to be serialized into JSON format. 58 | /// The response object to be serialized into JSON format. 59 | /// The HTTP method (e.g., GET, POST). 60 | /// The path of the HTTP request. 61 | /// The remote address (IP) from which the request originated. 62 | public WartEvent(object request, object response, string httpMethod, string httpPath, string remoteAddress) 63 | { 64 | this.EventId = Guid.NewGuid(); 65 | this.TimeStamp = DateTime.Now; 66 | this.UtcTimeStamp = DateTime.UtcNow; 67 | this.HttpMethod = httpMethod; 68 | this.HttpPath = httpPath; 69 | this.RemoteAddress = remoteAddress; 70 | this.JsonRequestPayload = SerializationHelper.Serialize(request); 71 | this.JsonResponsePayload = SerializationHelper.Serialize(response); 72 | } 73 | 74 | /// 75 | /// Returns the string representation of the object as a JSON string. 76 | /// The entire event is serialized to JSON using the . 77 | /// 78 | /// A JSON string representation of the . 79 | public override string ToString() 80 | { 81 | return SerializationHelper.Serialize(this); 82 | } 83 | 84 | /// 85 | /// Deserializes the request payload (JSON) into an object of the specified type. 86 | /// This method is useful for retrieving the original request object from the stored payload. 87 | /// 88 | /// The type to which the request payload will be deserialized. 89 | /// The deserialized object of type . 90 | public T GetRequestObject() where T : class 91 | { 92 | return SerializationHelper.Deserialize(JsonRequestPayload); 93 | } 94 | 95 | /// 96 | /// Deserializes the response payload (JSON) into an object of the specified type. 97 | /// This method is useful for retrieving the original response object from the stored payload. 98 | /// 99 | /// The type to which the response payload will be deserialized. 100 | /// The deserialized object of type . 101 | public T GetResponseObject() where T : class 102 | { 103 | return SerializationHelper.Deserialize(JsonResponsePayload); 104 | } 105 | 106 | /// 107 | /// Converts the WartEvent instance into a dictionary for flexible logging or data analysis. 108 | /// 109 | /// A dictionary representation of the event. 110 | public Dictionary ToDictionary() 111 | { 112 | return new Dictionary 113 | { 114 | { "EventId", EventId }, 115 | { "TimeStamp", TimeStamp }, 116 | { "UtcTimeStamp", UtcTimeStamp }, 117 | { "HttpMethod", HttpMethod }, 118 | { "HttpPath", HttpPath }, 119 | { "RemoteAddress", RemoteAddress }, 120 | { "JsonRequestPayload", JsonRequestPayload }, 121 | { "JsonResponsePayload", JsonResponsePayload }, 122 | { "ExtraInfo", ExtraInfo } 123 | }; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/WART-Core/Entity/WartEventWithFilters.cs: -------------------------------------------------------------------------------- 1 | // (c) 2024 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | using System.Collections.Generic; 5 | 6 | namespace WART_Core.Entity 7 | { 8 | /// 9 | /// Represents an event that contains additional filter metadata. 10 | /// 11 | public class WartEventWithFilters 12 | { 13 | /// 14 | /// The main WartEvent object. 15 | /// 16 | public WartEvent WartEvent { get; set; } 17 | 18 | /// 19 | /// A list of filters applied to the event. 20 | /// 21 | public List Filters { get; set; } 22 | 23 | /// 24 | /// Initializes a new instance of the WartEventWithFilters class. 25 | /// 26 | /// The WartEvent to associate with the filters. 27 | /// The list of filters applied to the event. 28 | public WartEventWithFilters(WartEvent wartEvent, List filters) 29 | { 30 | // Initialize the WartEvent and Filters properties 31 | WartEvent = wartEvent; 32 | Filters = filters; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/WART-Core/Enum/HubType.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | namespace WART_Core.Enum 4 | { 5 | /// 6 | /// Types of hubs supported. 7 | /// 8 | public enum HubType 9 | { 10 | /// 11 | /// Simple SignalR hub without authentication 12 | /// 13 | NoAuthentication, 14 | 15 | /// 16 | /// SignalR hub with JWT authentication 17 | /// 18 | JwtAuthentication, 19 | 20 | /// 21 | /// SignalR hub with Cookie authentication 22 | /// 23 | CookieAuthentication 24 | } 25 | } -------------------------------------------------------------------------------- /src/WART-Core/Filters/ExcludeWartAttribute.cs: -------------------------------------------------------------------------------- 1 | // (c) 2019 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | 5 | namespace WART_Core.Filters 6 | { 7 | /// 8 | /// A custom action filter that prevents the propagation of any SignalR events related to the action. 9 | /// When applied to an action, this filter ensures that no SignalR events are triggered or broadcasted 10 | /// as a result of the execution of the action. 11 | /// 12 | public class ExcludeWartAttribute : ActionFilterAttribute 13 | { 14 | public override void OnActionExecuting(ActionExecutingContext context) 15 | { 16 | base.OnActionExecuting(context); 17 | } 18 | 19 | public override void OnActionExecuted(ActionExecutedContext context) 20 | { 21 | base.OnActionExecuted(context); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/WART-Core/Filters/GroupWartAttribute.cs: -------------------------------------------------------------------------------- 1 | // (c) 2024 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | using System.Collections.Generic; 5 | 6 | namespace WART_Core.Filters 7 | { 8 | /// 9 | /// A custom action filter attribute used to direct SignalR events to a specific SignalR group or multiple groups. 10 | /// This attribute allows specifying a list of group names, which can be used to target SignalR events 11 | /// to one or more SignalR groups during the execution of an action. 12 | /// 13 | public class GroupWartAttribute : ActionFilterAttribute 14 | { 15 | public IReadOnlyList GroupNames { get; } 16 | 17 | // Constructor accepting a list of group names 18 | public GroupWartAttribute(params string[] groupNames) 19 | { 20 | GroupNames = new List(groupNames); 21 | } 22 | 23 | public override void OnActionExecuting(ActionExecutingContext context) 24 | { 25 | base.OnActionExecuting(context); 26 | } 27 | 28 | public override void OnActionExecuted(ActionExecutedContext context) 29 | { 30 | base.OnActionExecuted(context); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/WART-Core/Helpers/SerializationHelper.cs: -------------------------------------------------------------------------------- 1 | // (c) 2024 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using System; 4 | using System.Text.Json.Serialization; 5 | using System.Text.Json; 6 | 7 | namespace WART_Core.Helpers 8 | { 9 | public class SerializationHelper 10 | { 11 | // Default JSON serializer options to be used for serialization and deserialization. 12 | private static readonly JsonSerializerOptions DefaultOptions = new JsonSerializerOptions 13 | { 14 | WriteIndented = true, 15 | Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, 16 | PropertyNameCaseInsensitive = true, 17 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull 18 | }; 19 | 20 | /// 21 | /// Serializes the given object to a JSON string using default serialization options. 22 | /// 23 | /// The type of the object to serialize. 24 | /// The object to serialize. 25 | /// A JSON string representing the serialized object, or an empty string if serialization fails. 26 | public static string Serialize(T obj) 27 | { 28 | try 29 | { 30 | return JsonSerializer.Serialize(obj, DefaultOptions); 31 | } 32 | catch (Exception) 33 | { 34 | return string.Empty; 35 | } 36 | } 37 | 38 | /// 39 | /// Deserializes the given JSON string to an object of type T using default deserialization options. 40 | /// 41 | /// The type of the object to deserialize into. 42 | /// The JSON string to deserialize. 43 | /// An object of type T representing the deserialized data, or the default value of T if deserialization fails. 44 | public static T Deserialize(string jsonString) 45 | { 46 | try 47 | { 48 | return JsonSerializer.Deserialize(jsonString, DefaultOptions); 49 | } 50 | catch (Exception) 51 | { 52 | return default(T); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/WART-Core/Hubs/WartHub.cs: -------------------------------------------------------------------------------- 1 | // (c) 2019 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace WART_Core.Hubs 6 | { 7 | /// 8 | /// The WART SignalR hub. 9 | /// 10 | public class WartHub : WartHubBase 11 | { 12 | public WartHub(ILogger logger) : base(logger) { } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/WART-Core/Hubs/WartHubBase.cs: -------------------------------------------------------------------------------- 1 | // (c) 2024 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.SignalR; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace WART_Core.Hubs 10 | { 11 | /// 12 | /// Base class for WART SignalR hubs. 13 | /// Provides shared logic for managing connections, groups, and logging. 14 | /// 15 | public abstract class WartHubBase : Hub 16 | { 17 | /// 18 | /// Stores active connections with their respective identifiers. 19 | /// 20 | private static readonly ConcurrentDictionary _connections = new ConcurrentDictionary(); 21 | 22 | /// 23 | /// Logger instance for logging hub activities. 24 | /// 25 | private readonly ILogger _logger; 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | /// Logger instance for the hub. 31 | protected WartHubBase(ILogger logger) 32 | { 33 | _logger = logger; 34 | } 35 | 36 | /// 37 | /// Called when a new connection is established. 38 | /// Adds the connection to the dictionary and optionally assigns it to a group. 39 | /// 40 | /// A task that represents the asynchronous operation. 41 | public override async Task OnConnectedAsync() 42 | { 43 | var httpContext = Context.GetHttpContext(); 44 | var wartGroup = httpContext.Request.Query["WartGroup"].ToString(); 45 | var userName = Context.User?.Identity?.Name ?? "Anonymous"; 46 | 47 | _connections.TryAdd(Context.ConnectionId, userName); 48 | 49 | if (!string.IsNullOrEmpty(wartGroup)) 50 | { 51 | await AddToGroup(wartGroup); 52 | } 53 | 54 | _logger?.LogInformation($"OnConnect: ConnectionId={Context.ConnectionId}, User={userName}"); 55 | 56 | await base.OnConnectedAsync(); 57 | } 58 | 59 | /// 60 | /// Called when a connection is disconnected. 61 | /// Removes the connection from the dictionary and logs the event. 62 | /// 63 | /// The exception that occurred, if any. 64 | /// A task that represents the asynchronous operation. 65 | public override Task OnDisconnectedAsync(Exception exception) 66 | { 67 | _connections.TryRemove(Context.ConnectionId, out _); 68 | 69 | if (exception != null) 70 | { 71 | _logger?.LogWarning(exception, $"OnDisconnect with error: ConnectionId={Context.ConnectionId}"); 72 | } 73 | else 74 | { 75 | _logger?.LogInformation($"OnDisconnect: ConnectionId={Context.ConnectionId}"); 76 | } 77 | 78 | return base.OnDisconnectedAsync(exception); 79 | } 80 | 81 | /// 82 | /// Adds the current connection to a specified SignalR group. 83 | /// 84 | /// The name of the SignalR group to add the connection to. 85 | /// A task that represents the asynchronous operation. 86 | public async Task AddToGroup(string groupName) 87 | { 88 | if (string.IsNullOrWhiteSpace(groupName)) 89 | { 90 | _logger?.LogWarning("Attempted to add to a null or empty group."); 91 | return; 92 | } 93 | 94 | await Groups.AddToGroupAsync(Context.ConnectionId, groupName); 95 | 96 | _logger?.LogInformation($"Connection {Context.ConnectionId} added to group {groupName}"); 97 | } 98 | 99 | /// 100 | /// Removes the current connection from a specified SignalR group. 101 | /// 102 | /// The name of the SignalR group to remove the connection from. 103 | /// A task that represents the asynchronous operation. 104 | public async Task RemoveFromGroup(string groupName) 105 | { 106 | if (string.IsNullOrWhiteSpace(groupName)) 107 | { 108 | _logger?.LogWarning("Attempted to remove from a null or empty group."); 109 | return; 110 | } 111 | 112 | await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); 113 | 114 | _logger?.LogInformation($"Connection {Context.ConnectionId} removed from group {groupName}"); 115 | } 116 | 117 | /// 118 | /// Gets the current number of active connections. 119 | /// 120 | /// The count of active connections. 121 | public static int GetConnectionsCount() 122 | { 123 | return _connections.Count; 124 | } 125 | 126 | /// 127 | /// Returns a value indicating whether there are connected clients. 128 | /// 129 | public static bool HasConnectedClients => !_connections.IsEmpty; 130 | } 131 | } -------------------------------------------------------------------------------- /src/WART-Core/Hubs/WartHubCookie.cs: -------------------------------------------------------------------------------- 1 | // (c) 2025 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace WART_Core.Hubs 7 | { 8 | /// 9 | /// The WART SignalR hub with Cookie-based authentication. 10 | /// 11 | [Authorize] 12 | public class WartHubCookie : WartHubBase 13 | { 14 | public WartHubCookie(ILogger logger) : base(logger) { } 15 | } 16 | } -------------------------------------------------------------------------------- /src/WART-Core/Hubs/WartHubJwt.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Authentication.JwtBearer; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace WART_Core.Hubs 8 | { 9 | /// 10 | /// The WART SignalR hub with JWT authentication. 11 | /// 12 | [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] 13 | public class WartHubJwt : WartHubBase 14 | { 15 | public WartHubJwt(ILogger logger) : base(logger) { } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/WART-Core/Middleware/WartApplicationBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Builder; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using WART_Core.Authentication.Cookie; 8 | using WART_Core.Authentication.JWT; 9 | using WART_Core.Enum; 10 | using WART_Core.Hubs; 11 | 12 | namespace WART_Core.Middleware 13 | { 14 | public static class WartApplicationBuilderExtension 15 | { 16 | private const string DefaultHubName = "warthub"; 17 | 18 | /// 19 | /// Configures and adds the WART middleware to the IApplicationBuilder. 20 | /// This method sets up the default SignalR hub (warthub) without authentication. 21 | /// 22 | /// The IApplicationBuilder to configure the middleware pipeline. 23 | /// The updated IApplicationBuilder to continue configuration. 24 | public static IApplicationBuilder UseWartMiddleware(this IApplicationBuilder app) 25 | { 26 | app.UseRouting(); 27 | 28 | app.UseEndpoints(endpoints => 29 | { 30 | endpoints.MapControllers(); 31 | endpoints.MapHub($"/{DefaultHubName}"); 32 | }); 33 | 34 | app.UseForwardedHeaders(); 35 | 36 | return app; 37 | } 38 | 39 | /// 40 | /// Configures and adds the WART middleware to the IApplicationBuilder 41 | /// with a specified SignalR hub type. If the hub type requires authentication, 42 | /// JWT middleware will be included for secure access to the hub. 43 | /// 44 | /// The IApplicationBuilder to configure the middleware pipeline. 45 | /// The type of SignalR hub to configure, determining if authentication is required. 46 | /// The updated IApplicationBuilder to continue configuration. 47 | public static IApplicationBuilder UseWartMiddleware(this IApplicationBuilder app, HubType hubType) 48 | { 49 | app.UseRouting(); 50 | 51 | switch(hubType) 52 | { 53 | default: 54 | case HubType.NoAuthentication: 55 | { 56 | app.UseEndpoints(endpoints => 57 | { 58 | endpoints.MapControllers(); 59 | endpoints.MapHub($"/{DefaultHubName}"); 60 | }); 61 | break; 62 | } 63 | case HubType.JwtAuthentication: 64 | { 65 | app.UseJwtMiddleware(); 66 | app.UseEndpoints(endpoints => 67 | { 68 | endpoints.MapControllers(); 69 | endpoints.MapHub($"/{DefaultHubName}"); 70 | }); 71 | break; 72 | } 73 | case HubType.CookieAuthentication: 74 | { 75 | app.UseCookieMiddleware(); 76 | app.UseEndpoints(endpoints => 77 | { 78 | endpoints.MapControllers(); 79 | endpoints.MapHub($"/{DefaultHubName}"); 80 | }); 81 | break; 82 | } 83 | } 84 | 85 | app.UseForwardedHeaders(); 86 | 87 | return app; 88 | } 89 | 90 | /// 91 | /// Configures and adds the WART middleware to the IApplicationBuilder 92 | /// with a custom SignalR hub name. 93 | /// 94 | /// The IApplicationBuilder to configure the middleware pipeline. 95 | /// The custom SignalR hub name (URL path). 96 | /// The updated IApplicationBuilder to continue configuration. 97 | /// Thrown when the hub name is null or empty. 98 | public static IApplicationBuilder UseWartMiddleware(this IApplicationBuilder app, string hubName) 99 | { 100 | if (string.IsNullOrEmpty(hubName)) 101 | throw new ArgumentNullException("Invalid hub name"); 102 | 103 | app.UseRouting(); 104 | 105 | app.UseEndpoints(endpoints => 106 | { 107 | endpoints.MapControllers(); 108 | endpoints.MapHub($"/{hubName.Trim()}"); 109 | }); 110 | 111 | app.UseForwardedHeaders(); 112 | 113 | return app; 114 | } 115 | 116 | /// 117 | /// Configures and adds the WART middleware to the IApplicationBuilder 118 | /// with a list of SignalR hub names. This allows configuring multiple hubs 119 | /// at once by passing a list of custom names. 120 | /// 121 | /// The IApplicationBuilder to configure the middleware pipeline. 122 | /// The list of custom SignalR hub names (URL paths). 123 | /// The updated IApplicationBuilder to continue configuration. 124 | /// Thrown when the hub name list is null. 125 | public static IApplicationBuilder UseWartMiddleware(this IApplicationBuilder app, IEnumerable hubNameList) 126 | { 127 | ArgumentNullException.ThrowIfNull(hubNameList); 128 | 129 | app.UseRouting(); 130 | 131 | foreach (var hubName in hubNameList.Distinct()) 132 | { 133 | app.UseEndpoints(endpoints => 134 | { 135 | endpoints.MapControllers(); 136 | endpoints.MapHub($"/{hubName.Trim()}"); 137 | }); 138 | } 139 | 140 | app.UseForwardedHeaders(); 141 | 142 | return app; 143 | } 144 | 145 | /// 146 | /// Configures and adds the WART middleware to the IApplicationBuilder 147 | /// with a custom SignalR hub name and hub type (with or without authentication). 148 | /// 149 | /// The IApplicationBuilder to configure the middleware pipeline. 150 | /// The custom SignalR hub name (URL path). 151 | /// The type of SignalR hub to configure, determining if authentication is required. 152 | /// The updated IApplicationBuilder to continue configuration. 153 | /// Thrown when the hub name is null or empty. 154 | public static IApplicationBuilder UseWartMiddleware(this IApplicationBuilder app, string hubName, HubType hubType) 155 | { 156 | if (string.IsNullOrEmpty(hubName)) 157 | throw new ArgumentNullException("Invalid hub name"); 158 | 159 | app.UseRouting(); 160 | 161 | switch (hubType) 162 | { 163 | default: 164 | case HubType.NoAuthentication: 165 | { 166 | app.UseEndpoints(endpoints => 167 | { 168 | endpoints.MapControllers(); 169 | endpoints.MapHub($"/{hubName.Trim()}"); 170 | }); 171 | break; 172 | } 173 | case HubType.JwtAuthentication: 174 | { 175 | app.UseJwtMiddleware(); 176 | app.UseEndpoints(endpoints => 177 | { 178 | endpoints.MapControllers(); 179 | endpoints.MapHub($"/{hubName.Trim()}"); 180 | }); 181 | break; 182 | } 183 | case HubType.CookieAuthentication: 184 | { 185 | app.UseCookieMiddleware(); 186 | app.UseEndpoints(endpoints => 187 | { 188 | endpoints.MapControllers(); 189 | endpoints.MapHub($"/{hubName.Trim()}"); 190 | }); 191 | break; 192 | } 193 | } 194 | 195 | app.UseForwardedHeaders(); 196 | 197 | return app; 198 | } 199 | 200 | /// 201 | /// Configures and adds the WART middleware to the IApplicationBuilder 202 | /// with a list of custom SignalR hub names and a specified hub type 203 | /// (with or without authentication). This allows for multiple hubs 204 | /// with different authentication requirements to be configured. 205 | /// 206 | /// The IApplicationBuilder to configure the middleware pipeline. 207 | /// The list of custom SignalR hub names (URL paths). 208 | /// The type of SignalR hub to configure, determining if authentication is required. 209 | /// The updated IApplicationBuilder to continue configuration. 210 | /// Thrown when the hub name list is null. 211 | public static IApplicationBuilder UseWartMiddleware(this IApplicationBuilder app, IEnumerable hubNameList, HubType hubType) 212 | { 213 | ArgumentNullException.ThrowIfNull(hubNameList); 214 | 215 | app.UseRouting(); 216 | 217 | foreach (var hubName in hubNameList.Distinct()) 218 | { 219 | switch (hubType) 220 | { 221 | default: 222 | case HubType.NoAuthentication: 223 | { 224 | app.UseEndpoints(endpoints => 225 | { 226 | endpoints.MapControllers(); 227 | endpoints.MapHub($"/{hubName.Trim()}"); 228 | }); 229 | break; 230 | } 231 | case HubType.JwtAuthentication: 232 | { 233 | app.UseJwtMiddleware(); 234 | app.UseEndpoints(endpoints => 235 | { 236 | endpoints.MapControllers(); 237 | endpoints.MapHub($"/{hubName.Trim()}"); 238 | }); 239 | break; 240 | } 241 | case HubType.CookieAuthentication: 242 | { 243 | app.UseCookieMiddleware(); 244 | app.UseEndpoints(endpoints => 245 | { 246 | endpoints.MapControllers(); 247 | endpoints.MapHub($"/{hubName.Trim()}"); 248 | }); 249 | break; 250 | } 251 | } 252 | } 253 | 254 | app.UseForwardedHeaders(); 255 | 256 | return app; 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/WART-Core/Middleware/WartServiceCollectionExtension.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.HttpOverrides; 5 | using Microsoft.AspNetCore.ResponseCompression; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | using System; 9 | using System.Linq; 10 | using WART_Core.Authentication.Cookie; 11 | using WART_Core.Authentication.JWT; 12 | using WART_Core.Enum; 13 | using WART_Core.Hubs; 14 | using WART_Core.Services; 15 | 16 | namespace WART_Core.Middleware 17 | { 18 | public static class WartServiceCollectionExtension 19 | { 20 | /// 21 | /// Registers WART middleware dependencies to the IServiceCollection without authentication. 22 | /// This method configures forwarded headers, logging, SignalR, and response compression. 23 | /// 24 | /// The IServiceCollection to configure. 25 | /// The updated IServiceCollection with WART middleware dependencies. 26 | public static IServiceCollection AddWartMiddleware(this IServiceCollection services) 27 | { 28 | // Configure forwarded headers to support proxy scenarios (X-Forwarded-* headers). 29 | services.Configure(options => 30 | { 31 | options.ForwardedHeaders = 32 | ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; 33 | }); 34 | 35 | // Add console logging. 36 | services.AddLogging(configure => configure.AddConsole()); 37 | 38 | // Register WART event queue as a singleton service. 39 | services.AddSingleton(); 40 | 41 | // Register the WART event worker as a hosted service. 42 | services.AddHostedService>(); 43 | 44 | // Configure SignalR with custom options. 45 | services.AddSignalR(options => 46 | { 47 | options.EnableDetailedErrors = true; 48 | options.HandshakeTimeout = TimeSpan.FromSeconds(15); 49 | options.KeepAliveInterval = TimeSpan.FromSeconds(15); 50 | options.ClientTimeoutInterval = TimeSpan.FromSeconds(30); 51 | }); 52 | 53 | // Configure response compression for specific MIME types. 54 | services.AddResponseCompression(opts => 55 | { 56 | opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( 57 | new[] { "application/octet-stream" }); 58 | }); 59 | 60 | return services; 61 | } 62 | 63 | /// 64 | /// Registers WART middleware dependencies to the IServiceCollection, specifying the SignalR Hub type 65 | /// and optionally configuring JWT authentication with the provided token key. 66 | /// 67 | /// The IServiceCollection to configure. 68 | /// The type of SignalR Hub to use (with or without authentication). 69 | /// The JWT token key, if authentication is enabled. 70 | /// The updated IServiceCollection with WART middleware dependencies and optional JWT authentication. 71 | public static IServiceCollection AddWartMiddleware(this IServiceCollection services, HubType hubType, string tokenKey = "") 72 | { 73 | // Check the hub type to determine if authentication is required. 74 | switch(hubType) 75 | { 76 | default: 77 | case HubType.NoAuthentication: 78 | { 79 | // If no authentication is required, configure WART middleware without authentication. 80 | services.AddWartMiddleware(); 81 | break; 82 | } 83 | case HubType.JwtAuthentication: 84 | { 85 | // If authentication is required, configure JWT middleware for authentication. 86 | services.AddJwtMiddleware(tokenKey); 87 | break; 88 | } 89 | case HubType.CookieAuthentication: 90 | { 91 | // If authentication is required, configure Cookie middleware for authentication. 92 | services.AddCookieMiddleware(); 93 | break; 94 | } 95 | } 96 | 97 | return services; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/WART-Core/Serialization/JsonArrayOrObjectStringConverter.cs: -------------------------------------------------------------------------------- 1 | // (c) 2024 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using System; 4 | using System.Text.Json.Serialization; 5 | using System.Text.Json; 6 | 7 | namespace WART_Core.Serialization 8 | { 9 | public class JsonArrayOrObjectStringConverter : JsonConverter 10 | { 11 | public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 12 | { 13 | return reader.GetString(); 14 | } 15 | 16 | public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) 17 | { 18 | if (string.IsNullOrEmpty(value)) 19 | { 20 | writer.WriteStringValue(value); 21 | return; 22 | } 23 | 24 | using (JsonDocument doc = JsonDocument.Parse(value)) 25 | { 26 | if (doc.RootElement.ValueKind == JsonValueKind.Array) 27 | { 28 | writer.WriteStartArray(); 29 | foreach (var element in doc.RootElement.EnumerateArray()) 30 | { 31 | element.WriteTo(writer); 32 | } 33 | writer.WriteEndArray(); 34 | } 35 | else if (doc.RootElement.ValueKind == JsonValueKind.Object) 36 | { 37 | writer.WriteStartObject(); 38 | foreach (var property in doc.RootElement.EnumerateObject()) 39 | { 40 | property.WriteTo(writer); 41 | } 42 | writer.WriteEndObject(); 43 | } 44 | else 45 | { 46 | writer.WriteStringValue(value); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/WART-Core/Services/WartEventQueueService.cs: -------------------------------------------------------------------------------- 1 | // (c) 2024 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using System.Collections.Concurrent; 4 | using WART_Core.Entity; 5 | 6 | namespace WART_Core.Services 7 | { 8 | /// 9 | /// A service that manages a concurrent queue for WartEvent objects with filters. 10 | /// This class provides methods for enqueuing and dequeuing events. 11 | /// 12 | public class WartEventQueueService 13 | { 14 | // A thread-safe queue to hold WartEvent objects along with their associated filters. 15 | private readonly ConcurrentQueue _queue = new ConcurrentQueue(); 16 | 17 | /// 18 | /// Enqueues a WartEventWithFilters object to the queue. 19 | /// 20 | /// The WartEventWithFilters object to enqueue. 21 | public void Enqueue(WartEventWithFilters wartEventWithFilters) 22 | { 23 | // Adds the event with filters to the concurrent queue. 24 | _queue.Enqueue(wartEventWithFilters); 25 | } 26 | 27 | /// 28 | /// Attempts to dequeue a WartEventWithFilters object from the queue. 29 | /// 30 | /// The dequeued WartEventWithFilters object. 31 | /// True if an event was dequeued; otherwise, false. 32 | public bool TryDequeue(out WartEventWithFilters wartEventWithFilters) 33 | { 34 | // Attempts to remove and return the event with filters from the queue. 35 | return _queue.TryDequeue(out wartEventWithFilters); 36 | } 37 | 38 | /// 39 | /// Gets the current count of events in the queue. 40 | /// 41 | public int Count => _queue.Count; 42 | } 43 | } -------------------------------------------------------------------------------- /src/WART-Core/Services/WartEventWorker.cs: -------------------------------------------------------------------------------- 1 | // (c) 2024 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | using Microsoft.AspNetCore.SignalR; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Logging; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using WART_Core.Entity; 13 | using WART_Core.Filters; 14 | using WART_Core.Hubs; 15 | 16 | namespace WART_Core.Services 17 | { 18 | /// 19 | /// Background service that processes WartEvent objects from a queue and sends them to SignalR clients. 20 | /// 21 | public class WartEventWorker : BackgroundService where THub : Hub 22 | { 23 | private readonly WartEventQueueService _eventQueue; 24 | private readonly IHubContext _hubContext; 25 | private readonly ILogger> _logger; 26 | 27 | /// 28 | /// Constructor that initializes the worker with the event queue, hub context, and logger. 29 | /// 30 | public WartEventWorker(WartEventQueueService eventQueue, IHubContext hubContext, ILogger> logger) 31 | { 32 | _eventQueue = eventQueue; 33 | _hubContext = hubContext; 34 | _logger = logger; 35 | } 36 | 37 | /// 38 | /// Method that runs in the background to dequeue events and send them to SignalR clients. 39 | /// 40 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 41 | { 42 | _logger.LogInformation("WartEventWorker started."); 43 | 44 | // The worker will keep running as long as the cancellation token is not triggered. 45 | while (!stoppingToken.IsCancellationRequested) 46 | { 47 | // Check if there are any connected clients. 48 | if (!WartHubBase.HasConnectedClients) 49 | { 50 | await Task.Delay(500, stoppingToken); 51 | continue; 52 | } 53 | 54 | // Dequeue events and process them. 55 | while (_eventQueue.TryDequeue(out var wartEventWithFilters)) 56 | { 57 | try 58 | { 59 | // Extract the event and filters. 60 | var wartEvent = wartEventWithFilters.WartEvent; 61 | var filters = wartEventWithFilters.Filters; 62 | 63 | // Send the event to the SignalR hub. 64 | await SendToHub(wartEvent, filters); 65 | 66 | _logger.LogInformation("Event sent: {Event}", wartEvent); 67 | } 68 | catch (Exception ex) 69 | { 70 | // Log any errors that occur while sending the event. 71 | _logger.LogError(ex, "Error while sending event."); 72 | 73 | // Re-enqueue the event for retry 74 | // We lost the order of the events, but we can't lose the events 75 | _eventQueue.Enqueue(wartEventWithFilters); 76 | } 77 | } 78 | 79 | // Wait for 200 ms before checking for new events in the queue. 80 | await Task.Delay(200, stoppingToken); 81 | } 82 | 83 | _logger.LogInformation("WartEventWorker stopped."); 84 | } 85 | 86 | /// 87 | /// Sends the current event to the SignalR hub. 88 | /// This method determines if the event should be sent to specific groups or all clients. 89 | /// 90 | private async Task SendToHub(WartEvent wartEvent, List filters) 91 | { 92 | try 93 | { 94 | // Retrieve the target groups based on the filters. 95 | var groups = GetTargetGroups(filters); 96 | 97 | // If specific groups are defined, send the event to each group in parallel. 98 | if (groups.Count != 0) 99 | { 100 | var tasks = groups.Select(group => SendEventToGroup(wartEvent, group)); 101 | await Task.WhenAll(tasks); 102 | } 103 | else 104 | { 105 | // If no groups are defined, send the event to all clients. 106 | await SendEventToAllClients(wartEvent); 107 | } 108 | } 109 | catch (Exception ex) 110 | { 111 | // Log errors that occur while sending events to SignalR clients. 112 | _logger?.LogError(ex, "Error sending WartEvent to clients"); 113 | 114 | throw; 115 | } 116 | } 117 | 118 | /// 119 | /// Retrieves the list of groups that the WartEvent should be sent to, based on the provided filters. 120 | /// 121 | /// The list of filters that may contain group-related information. 122 | /// A list of group names to send the WartEvent to. 123 | private List GetTargetGroups(List filters) 124 | { 125 | var groups = new List(); 126 | 127 | // Check if there is a GroupWartAttribute filter indicating the groups. 128 | if (filters.Any(f => f.GetType().Name == nameof(GroupWartAttribute))) 129 | { 130 | var wartGroup = filters.FirstOrDefault(f => f.GetType() == typeof(GroupWartAttribute)) as GroupWartAttribute; 131 | if (wartGroup != null) 132 | { 133 | groups.AddRange(wartGroup.GroupNames); 134 | } 135 | } 136 | 137 | return groups; 138 | } 139 | 140 | /// 141 | /// Sends the WartEvent to a specific group of clients. 142 | /// 143 | private async Task SendEventToGroup(WartEvent wartEvent, string group) 144 | { 145 | // Send the event to the group using SignalR. 146 | await _hubContext?.Clients 147 | .Group(group) 148 | .SendAsync("Send", wartEvent.ToString()); 149 | 150 | // Log the event sent to the group. 151 | _logger?.LogInformation($"Group: {group}, WartEvent: {wartEvent}"); 152 | } 153 | 154 | /// 155 | /// Sends the WartEvent to all connected clients. 156 | /// 157 | private async Task SendEventToAllClients(WartEvent wartEvent) 158 | { 159 | // Send the event to all clients using SignalR. 160 | await _hubContext?.Clients.All 161 | .SendAsync("Send", wartEvent.ToString()); 162 | 163 | // Log the event sent to all clients. 164 | _logger?.LogInformation("Event: {EventName}, Details: {EventDetails}", nameof(WartEvent), wartEvent.ToString()); 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /src/WART-Core/WART-Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | WART_Core 6 | true 7 | Francesco Del Re 8 | Francesco Del Re 9 | Transforms REST APIs into SignalR events, bringing real-time interaction to your applications 10 | MIT 11 | https://github.com/engineering87/WART 12 | https://github.com/engineering87/WART 13 | LICENSE.txt 14 | 15 | 4.0.0.0 16 | 4.0.0.0 17 | 5.4.0 18 | icon.png 19 | README.md 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | True 32 | 33 | 34 | 35 | True 36 | \ 37 | 38 | 39 | True 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/WART-Tests/Entity/WartEventTests.cs: -------------------------------------------------------------------------------- 1 | // (c) 2024 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | namespace WART_Core.Entity.Tests 4 | { 5 | public class WartEventTests 6 | { 7 | [Fact] 8 | public void WartEvent_ConstructorWithParameters_ShouldSetProperties() 9 | { 10 | // Arrange 11 | string httpMethod = "GET"; 12 | string httpPath = "/api/users"; 13 | string remoteAddress = "127.0.0.1"; 14 | 15 | // Act 16 | var wartEvent = new WartEvent(httpMethod, httpPath, remoteAddress); 17 | 18 | // Assert 19 | Assert.NotEqual(Guid.Empty, wartEvent.EventId); 20 | Assert.NotEqual(default(DateTime), wartEvent.TimeStamp); 21 | Assert.NotEqual(default(DateTime), wartEvent.UtcTimeStamp); 22 | Assert.Equal(httpMethod, wartEvent.HttpMethod); 23 | Assert.Equal(httpPath, wartEvent.HttpPath); 24 | Assert.Equal(remoteAddress, wartEvent.RemoteAddress); 25 | Assert.Null(wartEvent.JsonRequestPayload); 26 | Assert.Null(wartEvent.JsonResponsePayload); 27 | Assert.Null(wartEvent.ExtraInfo); 28 | } 29 | 30 | [Fact] 31 | public void WartEvent_ConstructorWithRequestAndResponse_ShouldSetProperties() 32 | { 33 | // Arrange 34 | string httpMethod = "POST"; 35 | string httpPath = "/api/users"; 36 | string remoteAddress = "127.0.0.1"; 37 | var request = new { Name = "John", Age = 30 }; 38 | var response = new { Success = true }; 39 | 40 | // Act 41 | var wartEvent = new WartEvent(request, response, httpMethod, httpPath, remoteAddress); 42 | 43 | // Assert 44 | Assert.NotEqual(Guid.Empty, wartEvent.EventId); 45 | Assert.NotEqual(default(DateTime), wartEvent.TimeStamp); 46 | Assert.NotEqual(default(DateTime), wartEvent.UtcTimeStamp); 47 | Assert.Equal(httpMethod, wartEvent.HttpMethod); 48 | Assert.Equal(httpPath, wartEvent.HttpPath); 49 | Assert.Equal(remoteAddress, wartEvent.RemoteAddress); 50 | Assert.NotNull(wartEvent.JsonRequestPayload); 51 | Assert.NotNull(wartEvent.JsonResponsePayload); 52 | Assert.Null(wartEvent.ExtraInfo); 53 | } 54 | 55 | [Fact] 56 | public void WartEvent_ToString_ShouldReturnJsonSerialization() 57 | { 58 | // Arrange 59 | string httpMethod = "GET"; 60 | string httpPath = "/api/users"; 61 | string remoteAddress = "127.0.0.1"; 62 | var wartEvent = new WartEvent(httpMethod, httpPath, remoteAddress); 63 | 64 | // Act 65 | var jsonString = wartEvent.ToString(); 66 | 67 | // Assert 68 | Assert.NotNull(jsonString); 69 | Assert.Contains(httpMethod, jsonString); 70 | Assert.Contains(httpPath, jsonString); 71 | Assert.Contains(remoteAddress, jsonString); 72 | } 73 | 74 | [Fact] 75 | public void WartEvent_GetRequestObject_ShouldDeserializeJsonRequest() 76 | { 77 | // Arrange 78 | string httpMethod = "POST"; 79 | string httpPath = "/api/users"; 80 | string remoteAddress = "127.0.0.1"; 81 | var request = new { Name = "John", Age = 30 }; 82 | var response = new { Success = true }; 83 | var wartEvent = new WartEvent(request, response, httpMethod, httpPath, remoteAddress); 84 | 85 | // Act 86 | var deserializedRequest = wartEvent.GetRequestObject(); 87 | 88 | // Assert 89 | Assert.NotNull(deserializedRequest); 90 | } 91 | 92 | [Fact] 93 | public void WartEvent_GetResponseObject_ShouldDeserializeJsonResponse() 94 | { 95 | // Arrange 96 | string httpMethod = "POST"; 97 | string httpPath = "/api/users"; 98 | string remoteAddress = "127.0.0.1"; 99 | var request = new { Name = "John", Age = 30 }; 100 | var response = new { Success = true }; 101 | var wartEvent = new WartEvent(request, response, httpMethod, httpPath, remoteAddress); 102 | 103 | // Act 104 | var deserializedResponse = wartEvent.GetResponseObject(); 105 | 106 | // Assert 107 | Assert.NotNull(deserializedResponse); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/WART-Tests/Middleware/WartApplicationBuilderExtensionTests.cs: -------------------------------------------------------------------------------- 1 | // (c) 2024 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Mvc.Testing; 5 | using WART_Core.Middleware; 6 | using WART_Api; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace WART_Tests.Middleware 10 | { 11 | public class WartApplicationBuilderExtensionTests 12 | { 13 | [Fact] 14 | public async Task UseWartMiddleware_ShouldMapControllersAndHub() 15 | { 16 | var exception = await Record.ExceptionAsync(async () => 17 | { 18 | // Arrange 19 | var builder = new WebApplicationFactory() 20 | .WithWebHostBuilder(configure => 21 | { 22 | configure.ConfigureServices(services => 23 | { 24 | services.AddControllers(); 25 | services.AddSignalR(); 26 | }); 27 | configure.Configure(app => 28 | { 29 | app.UseWartMiddleware(); 30 | }); 31 | }); 32 | 33 | // Act 34 | var client = builder.CreateClient(); 35 | var response = await client.GetAsync("/signalr"); 36 | }); 37 | 38 | // Assert 39 | Assert.Null(exception); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/WART-Tests/Middleware/WartServiceCollectionExtensionTests.cs: -------------------------------------------------------------------------------- 1 | // (c) 2024 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.HttpOverrides; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | using WART_Core.Middleware; 8 | using Microsoft.AspNetCore.SignalR; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace WART_Tests.Middleware 12 | { 13 | public class WartServiceCollectionExtensionTests 14 | { 15 | [Fact] 16 | public void AddWartMiddleware_ShouldConfigureForwardedHeadersOptions() 17 | { 18 | // Arrange 19 | var services = new ServiceCollection(); 20 | 21 | // Act 22 | services.AddWartMiddleware(); 23 | 24 | // Assert 25 | var serviceProvider = services.BuildServiceProvider(); 26 | var options = serviceProvider.GetService>(); 27 | Assert.NotNull(options); 28 | Assert.Equal(ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto, options.Value.ForwardedHeaders); 29 | } 30 | 31 | [Fact] 32 | public void AddWartMiddleware_ShouldAddConsoleLogging() 33 | { 34 | // Arrange 35 | var services = new ServiceCollection(); 36 | 37 | // Act 38 | services.AddWartMiddleware(); 39 | 40 | // Assert 41 | var serviceProvider = services.BuildServiceProvider(); 42 | var loggerFactory = serviceProvider.GetService(); 43 | var logger = loggerFactory?.CreateLogger("TestLogger"); 44 | Assert.NotNull(logger); 45 | } 46 | 47 | [Fact] 48 | public void AddWartMiddleware_ShouldAddSignalRWithDetailedErrorsEnabled() 49 | { 50 | // Arrange 51 | var services = new ServiceCollection(); 52 | 53 | // Act 54 | services.AddWartMiddleware(); 55 | 56 | // Assert 57 | var serviceProvider = services.BuildServiceProvider(); 58 | var options = serviceProvider.GetService>(); 59 | Assert.NotNull(options); 60 | Assert.True(options.Value.EnableDetailedErrors); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/WART-Tests/WART-Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | WART_Tests 6 | enable 7 | enable 8 | 9 | false 10 | true 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/WART-WebApiRealTime.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.34928.147 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WART-Api", "WART-WebApiRealTime\WART-Api.csproj", "{2F57B8CA-6C41-485D-82D1-F9039FD11360}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WART-Client", "WART-Client\WART-Client.csproj", "{57AD2261-F3CA-49C6-9BC0-FCB2DC5A86E2}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WART-Core", "WART-Core\WART-Core.csproj", "{7588AAE3-E882-468E-81A0-90B8F92D7B72}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WART-Tests", "WART-Tests\WART-Tests.csproj", "{A80F18C6-1FF2-4F13-A65D-9134EE44158A}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {2F57B8CA-6C41-485D-82D1-F9039FD11360}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2F57B8CA-6C41-485D-82D1-F9039FD11360}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2F57B8CA-6C41-485D-82D1-F9039FD11360}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2F57B8CA-6C41-485D-82D1-F9039FD11360}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {57AD2261-F3CA-49C6-9BC0-FCB2DC5A86E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {57AD2261-F3CA-49C6-9BC0-FCB2DC5A86E2}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {57AD2261-F3CA-49C6-9BC0-FCB2DC5A86E2}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {57AD2261-F3CA-49C6-9BC0-FCB2DC5A86E2}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {7588AAE3-E882-468E-81A0-90B8F92D7B72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {7588AAE3-E882-468E-81A0-90B8F92D7B72}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {7588AAE3-E882-468E-81A0-90B8F92D7B72}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {7588AAE3-E882-468E-81A0-90B8F92D7B72}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {A80F18C6-1FF2-4F13-A65D-9134EE44158A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {A80F18C6-1FF2-4F13-A65D-9134EE44158A}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {A80F18C6-1FF2-4F13-A65D-9134EE44158A}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {A80F18C6-1FF2-4F13-A65D-9134EE44158A}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {2D31C9AA-45A6-4053-AEA0-31D15E8DCFC1} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /src/WART-WebApiRealTime/Controllers/TestController.cs: -------------------------------------------------------------------------------- 1 | // (c) 2019 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.SignalR; 5 | using Microsoft.Extensions.Logging; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using WART_Api.Entity; 9 | using WART_Core.Controllers; 10 | using WART_Core.Filters; 11 | using WART_Core.Hubs; 12 | 13 | namespace WART_Api.Controllers 14 | { 15 | /// 16 | /// A simple controller example extended by the WartController. 17 | /// 18 | [ApiController] 19 | [Route("api/[controller]")] 20 | public class TestController : WartController 21 | { 22 | private static List Items = new List 23 | { 24 | new TestEntity { Id = 1, Param = "Item1" }, 25 | new TestEntity { Id = 2, Param = "Item2" }, 26 | new TestEntity { Id = 3, Param = "Item3" } 27 | }; 28 | 29 | public TestController(IHubContext messageHubContext, ILogger logger) : base(messageHubContext, logger) 30 | { 31 | } 32 | 33 | [HttpGet] 34 | public IEnumerable Get() 35 | { 36 | return Items; 37 | } 38 | 39 | [HttpGet("{id}")] 40 | [ExcludeWart] 41 | public ActionResult Get(int id) 42 | { 43 | var item = Items.FirstOrDefault(x => x.Id == id); 44 | if (item == null) 45 | { 46 | return NotFound(); 47 | } 48 | return item; 49 | } 50 | 51 | [HttpPost] 52 | [GroupWart("SampleGroupName")] 53 | public ActionResult Post([FromBody] TestEntity entity) 54 | { 55 | Items.Add(entity); 56 | return entity; 57 | } 58 | 59 | [HttpPatch("{id}")] 60 | public ActionResult Patch(int id, [FromBody] TestEntity entity) 61 | { 62 | var item = Items.FirstOrDefault(x => x.Id == id); 63 | if (item == null) 64 | { 65 | return NotFound(); 66 | } 67 | item.Param = entity.Param; 68 | return item; 69 | } 70 | 71 | [HttpPut("{id}")] 72 | public ActionResult Put(int id, [FromBody] TestEntity entity) 73 | { 74 | var item = Items.FirstOrDefault(x => x.Id == id); 75 | if (item == null) 76 | { 77 | return NotFound(); 78 | } 79 | item.Param = entity.Param; 80 | return item; 81 | } 82 | 83 | [HttpDelete("{id}")] 84 | public ActionResult Delete(int id) 85 | { 86 | var item = Items.FirstOrDefault(x => x.Id == id); 87 | if (item == null) 88 | { 89 | return NotFound(); 90 | } 91 | Items.Remove(item); 92 | return item; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/WART-WebApiRealTime/Controllers/TestCookieController.cs: -------------------------------------------------------------------------------- 1 | // (c) 2025 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Microsoft.AspNetCore.Authentication; 7 | using System.Security.Claims; 8 | using System.Threading.Tasks; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.AspNetCore.SignalR; 11 | using Microsoft.Extensions.Logging; 12 | using WART_Api.Entity; 13 | using WART_Core.Controllers; 14 | using WART_Core.Filters; 15 | using WART_Core.Hubs; 16 | 17 | namespace WART_Api.Controllers 18 | { 19 | /// 20 | /// A simple controller example extended by the WartController with Cookie authentication. 21 | /// 22 | [ApiController] 23 | [Route("api/[controller]")] 24 | public class TestCookieController : WartControllerCookie 25 | { 26 | private static List Items = new List 27 | { 28 | new TestEntity { Id = 1, Param = "Item1" }, 29 | new TestEntity { Id = 2, Param = "Item2" }, 30 | new TestEntity { Id = 3, Param = "Item3" } 31 | }; 32 | 33 | public TestCookieController(IHubContext messageHubContext, ILogger logger) : base(messageHubContext, logger) 34 | { 35 | } 36 | 37 | // Login endpoint: issues authentication cookie using "WartCookieAuth" scheme 38 | [HttpPost("login")] 39 | [ExcludeWart] // Exclude from event interception if using custom filters 40 | public async Task Login([FromForm] string username, [FromForm] string password) 41 | { 42 | if (!string.IsNullOrEmpty(username) && !string.IsNullOrEmpty(password)) 43 | { 44 | var claims = new List 45 | { 46 | new Claim(ClaimTypes.Name, username) 47 | }; 48 | 49 | var identity = new ClaimsIdentity(claims, "WartCookieAuth"); 50 | var principal = new ClaimsPrincipal(identity); 51 | 52 | await HttpContext.SignInAsync("WartCookieAuth", principal, new AuthenticationProperties 53 | { 54 | IsPersistent = true, 55 | ExpiresUtc = DateTime.UtcNow.AddHours(1) 56 | }); 57 | 58 | return Ok("Login successful."); 59 | } 60 | 61 | return Unauthorized("Invalid credentials."); 62 | } 63 | 64 | [HttpGet] 65 | public IEnumerable Get() 66 | { 67 | return Items; 68 | } 69 | 70 | [HttpGet("{id}")] 71 | [ExcludeWart] 72 | public ActionResult Get(int id) 73 | { 74 | var item = Items.FirstOrDefault(x => x.Id == id); 75 | if (item == null) 76 | { 77 | return NotFound(); 78 | } 79 | return item; 80 | } 81 | 82 | [HttpPost] 83 | [GroupWart("SampleGroupName")] 84 | public ActionResult Post([FromBody] TestEntity entity) 85 | { 86 | Items.Add(entity); 87 | return entity; 88 | } 89 | 90 | [HttpPatch("{id}")] 91 | public ActionResult Patch(int id, [FromBody] TestEntity entity) 92 | { 93 | var item = Items.FirstOrDefault(x => x.Id == id); 94 | if (item == null) 95 | { 96 | return NotFound(); 97 | } 98 | item.Param = entity.Param; 99 | return item; 100 | } 101 | 102 | [HttpPut("{id}")] 103 | public ActionResult Put(int id, [FromBody] TestEntity entity) 104 | { 105 | var item = Items.FirstOrDefault(x => x.Id == id); 106 | if (item == null) 107 | { 108 | return NotFound(); 109 | } 110 | item.Param = entity.Param; 111 | return item; 112 | } 113 | 114 | [HttpDelete("{id}")] 115 | public ActionResult Delete(int id) 116 | { 117 | var item = Items.FirstOrDefault(x => x.Id == id); 118 | if (item == null) 119 | { 120 | return NotFound(); 121 | } 122 | Items.Remove(item); 123 | return item; 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /src/WART-WebApiRealTime/Controllers/TestJwtController.cs: -------------------------------------------------------------------------------- 1 | // (c) 2019 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.SignalR; 7 | using Microsoft.Extensions.Logging; 8 | using WART_Api.Entity; 9 | using WART_Core.Controllers; 10 | using WART_Core.Filters; 11 | using WART_Core.Hubs; 12 | 13 | namespace WART_Api.Controllers 14 | { 15 | /// 16 | /// A simple controller example extended by the WartController with JWT authentication. 17 | /// 18 | [ApiController] 19 | [Route("api/[controller]")] 20 | public class TestJwtController : WartControllerJwt 21 | { 22 | private static List Items = new List 23 | { 24 | new TestEntity { Id = 1, Param = "Item1" }, 25 | new TestEntity { Id = 2, Param = "Item2" }, 26 | new TestEntity { Id = 3, Param = "Item3" } 27 | }; 28 | 29 | public TestJwtController(IHubContext messageHubContext, ILogger logger) : base(messageHubContext, logger) 30 | { 31 | } 32 | 33 | [HttpGet] 34 | public IEnumerable Get() 35 | { 36 | return Items; 37 | } 38 | 39 | [HttpGet("{id}")] 40 | [ExcludeWart] 41 | public ActionResult Get(int id) 42 | { 43 | var item = Items.FirstOrDefault(x => x.Id == id); 44 | if (item == null) 45 | { 46 | return NotFound(); 47 | } 48 | return item; 49 | } 50 | 51 | [HttpPost] 52 | [GroupWart("SampleGroupName")] 53 | public ActionResult Post([FromBody] TestEntity entity) 54 | { 55 | Items.Add(entity); 56 | return entity; 57 | } 58 | 59 | [HttpPatch("{id}")] 60 | public ActionResult Patch(int id, [FromBody] TestEntity entity) 61 | { 62 | var item = Items.FirstOrDefault(x => x.Id == id); 63 | if (item == null) 64 | { 65 | return NotFound(); 66 | } 67 | item.Param = entity.Param; 68 | return item; 69 | } 70 | 71 | [HttpPut("{id}")] 72 | public ActionResult Put(int id, [FromBody] TestEntity entity) 73 | { 74 | var item = Items.FirstOrDefault(x => x.Id == id); 75 | if (item == null) 76 | { 77 | return NotFound(); 78 | } 79 | item.Param = entity.Param; 80 | return item; 81 | } 82 | 83 | [HttpDelete("{id}")] 84 | public ActionResult Delete(int id) 85 | { 86 | var item = Items.FirstOrDefault(x => x.Id == id); 87 | if (item == null) 88 | { 89 | return NotFound(); 90 | } 91 | Items.Remove(item); 92 | return item; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/WART-WebApiRealTime/Entity/TestEntity.cs: -------------------------------------------------------------------------------- 1 | // (c) 2019 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using System; 4 | 5 | namespace WART_Api.Entity 6 | { 7 | [Serializable] 8 | public class TestEntity 9 | { 10 | public int Id { get; set; } 11 | public DateTime Date { get; set; } 12 | public string Param { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/WART-WebApiRealTime/Program.cs: -------------------------------------------------------------------------------- 1 | // (c) 2019 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace WART_Api 7 | { 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | CreateHostBuilder(args).Build().Run(); 13 | } 14 | 15 | public static IHostBuilder CreateHostBuilder(string[] args) => 16 | Host.CreateDefaultBuilder(args) 17 | .ConfigureWebHostDefaults(webBuilder => 18 | { 19 | webBuilder.UseStartup(); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/WART-WebApiRealTime/Startup.cs: -------------------------------------------------------------------------------- 1 | // (c) 2021 Francesco Del Re 2 | // This code is licensed under MIT license (see LICENSE.txt for details) 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.OpenApi.Models; 9 | using System; 10 | using System.Collections.Generic; 11 | using WART_Core.Enum; 12 | using WART_Core.Middleware; 13 | 14 | namespace WART_Api 15 | { 16 | public class Startup 17 | { 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public IConfiguration Configuration { get; } 24 | 25 | // This method gets called by the runtime. Use this method to add services to the container. 26 | public void ConfigureServices(IServiceCollection services) 27 | { 28 | services.AddControllers(); 29 | 30 | // add the Wart middleware service extension 31 | 32 | // default without authentication 33 | //services.AddWartMiddleware(); 34 | 35 | // with JWT authentication 36 | //services.AddWartMiddleware(hubType: HubType.JwtAuthentication, tokenKey: "dn3341fmcscscwe28419brhwbwgbss4t"); 37 | 38 | // with Cookie authentication 39 | services.AddWartMiddleware(hubType: HubType.CookieAuthentication); 40 | 41 | // Register the Swagger generator, defining 1 or more Swagger documents 42 | services.AddSwaggerGen(c => 43 | { 44 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "WART-Api", Version = "v1" }); 45 | }); 46 | } 47 | 48 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 49 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 50 | { 51 | if (env.IsDevelopment()) 52 | { 53 | app.UseDeveloperExceptionPage(); 54 | } 55 | else 56 | { 57 | app.UseExceptionHandler("/Error"); 58 | 59 | app.UseHsts(); 60 | } 61 | 62 | // Enable middleware to serve generated Swagger as a JSON endpoint. 63 | app.UseSwagger(); 64 | 65 | // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 66 | // specifying the Swagger JSON endpoint. 67 | app.UseSwaggerUI(c => 68 | { 69 | c.SwaggerEndpoint("/swagger/v1/swagger.json", "WART-Api"); 70 | c.RoutePrefix = string.Empty; 71 | }); 72 | 73 | // use the Wart middleware builder extension 74 | // default without authentication 75 | //app.UseWartMiddleware(); 76 | 77 | // with JWT authentication 78 | //app.UseWartMiddleware(HubType.JwtAuthentication); 79 | 80 | // with Cookie authentication 81 | app.UseWartMiddleware(HubType.CookieAuthentication); 82 | 83 | // multiple hub with authentication 84 | //var hubNameList = new List 85 | //{ 86 | // "warthub", 87 | // "warthub_clone" 88 | //}; 89 | //app.UseWartMiddleware(hubNameList, HubType.JwtAuthentication); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/WART-WebApiRealTime/WART-Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | WART_Api 6 | False 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/WART-WebApiRealTime/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/WART-WebApiRealTime/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /wart_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/engineering87/WART/7f161fd42817aba5d706630c1e6dcd16fe885ae5/wart_logo.jpg --------------------------------------------------------------------------------