├── .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 | [](https://opensource.org/licenses/MIT)
4 | [](https://www.nuget.org/packages/WART-Core)
5 | 
6 | [](https://github.com/engineering87/WART/issues)
7 | [](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