├── .dockerignore ├── .github └── workflows │ └── main.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Ntrada.sln ├── README.md ├── extensions ├── Ntrada.Extensions.Cors │ ├── CorsExtension.cs │ ├── CorsOptions.cs │ ├── Ntrada.Extensions.Cors.csproj │ └── cors.yml ├── Ntrada.Extensions.CustomErrors │ ├── CustomErrorsExtension.cs │ ├── CustomErrorsOptions.cs │ ├── ErrorHandlerMiddleware.cs │ ├── Ntrada.Extensions.CustomErrors.csproj │ └── customErrors.yml ├── Ntrada.Extensions.Jwt │ ├── JwtExtension.cs │ ├── JwtOptions.cs │ ├── Ntrada.Extensions.Jwt.csproj │ └── jwt.yml ├── Ntrada.Extensions.RabbitMq │ ├── Clients │ │ └── RabbitMqClient.cs │ ├── Contexts │ │ ├── NullContext.cs │ │ ├── NullContextBuilder.cs │ │ └── NullSpanContextBuilder.cs │ ├── Handlers │ │ └── RabbitMqHandler.cs │ ├── IContextBuilder.cs │ ├── IRabbitMqClient.cs │ ├── ISpanContextBuilder.cs │ ├── Ntrada.Extensions.RabbitMq.csproj │ ├── RabbitMqExtension.cs │ ├── RabbitMqOptions.cs │ └── rabbitmq.yml ├── Ntrada.Extensions.Swagger │ ├── Ntrada.Extensions.Swagger.csproj │ ├── SwaggerExtension.cs │ ├── SwaggerOptions.cs │ ├── WebApiDocumentFilter.cs │ └── rabbitmq.yml └── Ntrada.Extensions.Tracing │ ├── DefaultTracer.cs │ ├── JaegerHttpMiddleware.cs │ ├── Ntrada.Extensions.Tracing.csproj │ ├── TracingExtension.cs │ ├── TracingOptions.cs │ └── tracing.yml ├── samples ├── Ntrada.Samples.Api │ ├── Ntrada.Samples.Api.csproj │ ├── Ntrada.Samples.Api.rest │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ └── ntrada.yml └── Ntrada.Samples.Services.Orders │ ├── Controllers │ └── OrdersController.cs │ ├── Models │ └── OrderDto.cs │ ├── Ntrada.Samples.Services.Orders.csproj │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Requests │ └── CreateOrder.cs │ ├── Services │ ├── IOrdersService.cs │ └── OrdersService.cs │ ├── Startup.cs │ └── appsettings.json ├── src ├── Ntrada.Host │ ├── Ntrada.Host.csproj │ ├── Program.cs │ └── ntrada.yml └── Ntrada │ ├── Auth │ ├── AuthenticationManager.cs │ ├── AuthorizationManager.cs │ └── PolicyManager.cs │ ├── Configuration │ ├── Auth.cs │ ├── Http.cs │ ├── LoadBalancer.cs │ ├── Module.cs │ ├── OnError.cs │ ├── OnSuccess.cs │ ├── Policy.cs │ ├── ResourceId.cs │ ├── Route.cs │ └── Service.cs │ ├── Error.cs │ ├── ExecutionData.cs │ ├── Extensions │ ├── EnabledExtension.cs │ ├── ExtensionOptions.cs │ └── ExtensionProvider.cs │ ├── Handlers │ ├── DownstreamHandler.cs │ └── ReturnValueHandler.cs │ ├── Helpers │ └── Extensions.cs │ ├── Hooks │ ├── IHttpRequestHook.cs │ ├── IHttpResponseHook.cs │ ├── IRequestHook.cs │ └── IResponseHook.cs │ ├── IAuthenticationManager.cs │ ├── IAuthorizationManager.cs │ ├── IDownstreamBuilder.cs │ ├── IEnabledExtension.cs │ ├── IExtension.cs │ ├── IExtensionOptions.cs │ ├── IExtensionProvider.cs │ ├── IHandler.cs │ ├── IOptions.cs │ ├── IOptionsProvider.cs │ ├── IPayloadBuilder.cs │ ├── IPayloadManager.cs │ ├── IPayloadTransformer.cs │ ├── IPayloadValidator.cs │ ├── IPolicyManager.cs │ ├── IRequestExecutionValidator.cs │ ├── IRequestHandlerManager.cs │ ├── IRequestProcessor.cs │ ├── IRouteConfigurator.cs │ ├── IRouteProvider.cs │ ├── ISchemaValidator.cs │ ├── IUpstreamBuilder.cs │ ├── IValueProvider.cs │ ├── Ntrada.csproj │ ├── NtradaExtensions.cs │ ├── Options │ ├── NtradaOptions.cs │ └── OptionsProvider.cs │ ├── PayloadSchema.cs │ ├── Requests │ ├── PayloadBuilder.cs │ ├── PayloadManager.cs │ ├── PayloadTransformer.cs │ ├── PayloadValidator.cs │ ├── RequestHandlerManager.cs │ ├── RequestProcessor.cs │ ├── SchemaValidator.cs │ └── ValueProvider.cs │ ├── RouteConfig.cs │ ├── Routing │ ├── DownstreamBuilder.cs │ ├── RequestExecutionValidator.cs │ ├── RouteConfigurator.cs │ ├── RouteProvider.cs │ └── UpstreamBuilder.cs │ └── WebApi │ ├── WebApiEndpointDefinition.cs │ ├── WebApiEndpointDefinitions.cs │ ├── WebApiEndpointParameter.cs │ └── WebApiEndpointResponse.cs └── tests ├── Ntrada.Tests.Integration └── Ntrada.Tests.Integration.csproj └── Ntrada.Tests.Unit ├── Auth ├── AuthenticationManagerTests.cs ├── AuthorizationManagerTests.cs └── PolicyManagerTests.cs ├── Extensions └── ExtensionProviderTests.cs ├── Handlers ├── HandlerTestsBase.cs └── ReturnValueHandlerTests.cs └── Ntrada.Tests.Unit.csproj /.dockerignore: -------------------------------------------------------------------------------- 1 | **/obj/ 2 | **/bin/ -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: Setup .NET Core 11 | uses: actions/setup-dotnet@v1 12 | with: 13 | dotnet-version: 2.2.108 14 | - name: Build with dotnet 15 | run: dotnet build --configuration Release 16 | - name: Tests 17 | run: dotnet test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # Visual Studio code coverage results 114 | *.coverage 115 | *.coveragexml 116 | 117 | # NCrunch 118 | _NCrunch_* 119 | .*crunch*.local.xml 120 | nCrunchTemp_* 121 | 122 | # MightyMoose 123 | *.mm.* 124 | AutoTest.Net/ 125 | 126 | # Web workbench (sass) 127 | .sass-cache/ 128 | 129 | # Installshield output folder 130 | [Ee]xpress/ 131 | 132 | # DocProject is a documentation generator add-in 133 | DocProject/buildhelp/ 134 | DocProject/Help/*.HxT 135 | DocProject/Help/*.HxC 136 | DocProject/Help/*.hhc 137 | DocProject/Help/*.hhk 138 | DocProject/Help/*.hhp 139 | DocProject/Help/Html2 140 | DocProject/Help/html 141 | 142 | # Click-Once directory 143 | publish/ 144 | 145 | # Publish Web Output 146 | *.[Pp]ublish.xml 147 | *.azurePubxml 148 | # TODO: Comment the next line if you want to checkin your web deploy settings 149 | # but database connection strings (with potential passwords) will be unencrypted 150 | *.pubxml 151 | *.publishproj 152 | 153 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 154 | # checkin your Azure Web App publish settings, but sensitive information contained 155 | # in these scripts will be unencrypted 156 | PublishScripts/ 157 | 158 | # NuGet Packages 159 | *.nupkg 160 | # The packages folder can be ignored because of Package Restore 161 | **/packages/* 162 | # except build/, which is used as an MSBuild target. 163 | !**/packages/build/ 164 | # Uncomment if necessary however generally it will be regenerated when needed 165 | #!**/packages/repositories.config 166 | # NuGet v3's project.json files produces more ignoreable files 167 | *.nuget.props 168 | *.nuget.targets 169 | 170 | # Microsoft Azure Build Output 171 | csx/ 172 | *.build.csdef 173 | 174 | # Microsoft Azure Emulator 175 | ecf/ 176 | rcf/ 177 | 178 | # Windows Store app package directories and files 179 | AppPackages/ 180 | BundleArtifacts/ 181 | Package.StoreAssociation.xml 182 | _pkginfo.txt 183 | 184 | # Visual Studio cache files 185 | # files ending in .cache can be ignored 186 | *.[Cc]ache 187 | # but keep track of directories ending in .cache 188 | !*.[Cc]ache/ 189 | 190 | # Others 191 | ClientBin/ 192 | ~$* 193 | *~ 194 | *.dbmdl 195 | *.dbproj.schemaview 196 | *.jfm 197 | *.pfx 198 | *.publishsettings 199 | node_modules/ 200 | orleans.codegen.cs 201 | 202 | # Since there are multiple workflows, uncomment next line to ignore bower_components 203 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 204 | #bower_components/ 205 | 206 | # RIA/Silverlight projects 207 | Generated_Code/ 208 | 209 | # Backup & report files from converting an old project file 210 | # to a newer Visual Studio version. Backup files are not needed, 211 | # because we have git ;-) 212 | _UpgradeReport_Files/ 213 | Backup*/ 214 | UpgradeLog*.XML 215 | UpgradeLog*.htm 216 | 217 | # SQL Server files 218 | *.mdf 219 | *.ldf 220 | 221 | # Business Intelligence projects 222 | *.rdl.data 223 | *.bim.layout 224 | *.bim_*.settings 225 | 226 | # Microsoft Fakes 227 | FakesAssemblies/ 228 | 229 | # GhostDoc plugin setting file 230 | *.GhostDoc.xml 231 | 232 | # Node.js Tools for Visual Studio 233 | .ntvs_analysis.dat 234 | 235 | # Visual Studio 6 build log 236 | *.plg 237 | 238 | # Visual Studio 6 workspace options file 239 | *.opt 240 | 241 | # Visual Studio LightSwitch build output 242 | **/*.HTMLClient/GeneratedArtifacts 243 | **/*.DesktopClient/GeneratedArtifacts 244 | **/*.DesktopClient/ModelManifest.xml 245 | **/*.Server/GeneratedArtifacts 246 | **/*.Server/ModelManifest.xml 247 | _Pvt_Extensions 248 | 249 | # Paket dependency manager 250 | .paket/paket.exe 251 | paket-files/ 252 | 253 | # FAKE - F# Make 254 | .fake/ 255 | 256 | # JetBrains Rider 257 | .idea/ 258 | *.sln.iml 259 | 260 | # CodeRush 261 | .cr/ 262 | 263 | # Python Tools for Visual Studio (PTVS) 264 | __pycache__/ 265 | *.pyc 266 | 267 | .vscode/* 268 | !.vscode/settings.json 269 | !.vscode/tasks.json 270 | !.vscode/launch.json 271 | 272 | NuGet.config 273 | appsettings.local.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build 2 | WORKDIR /publish 3 | COPY . . 4 | RUN dotnet publish src/Ntrada.Host -c Release -o out 5 | 6 | FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 7 | WORKDIR /ntrada 8 | COPY --from=build /publish/out . 9 | ENV ASPNETCORE_URLS http://*:80 10 | ENV ASPNETCORE_ENVIRONMENT docker 11 | ENTRYPOINT dotnet Ntrada.Host.dll -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 snatch.dev 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 | -------------------------------------------------------------------------------- /Ntrada.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{10BC7168-BE16-4A3C-A7CC-F122C83E08F5}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada", "src\Ntrada\Ntrada.csproj", "{BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada.Host", "src\Ntrada.Host\Ntrada.Host.csproj", "{0B949CC3-A1AF-4460-A942-D03ADA500BC0}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{D1922E6F-C1D9-4C38-B667-36E417734DE8}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada.Tests.Integration", "tests\Ntrada.Tests.Integration\Ntrada.Tests.Integration.csproj", "{8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada.Tests.Unit", "tests\Ntrada.Tests.Unit\Ntrada.Tests.Unit.csproj", "{254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{367233AC-1E0D-4FA2-8701-09702663DD54}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada.Samples.Api", "samples\Ntrada.Samples.Api\Ntrada.Samples.Api.csproj", "{5195DD55-8257-4238-9B92-33E8574BBAA0}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "extensions", "extensions", "{6E8C9C7B-7249-41CD-A466-C11A8644400B}" 23 | EndProject 24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada.Extensions.Jwt", "extensions\Ntrada.Extensions.Jwt\Ntrada.Extensions.Jwt.csproj", "{B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}" 25 | EndProject 26 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada.Extensions.RabbitMq", "extensions\Ntrada.Extensions.RabbitMq\Ntrada.Extensions.RabbitMq.csproj", "{4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}" 27 | EndProject 28 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada.Extensions.CustomErrors", "extensions\Ntrada.Extensions.CustomErrors\Ntrada.Extensions.CustomErrors.csproj", "{AB8D52BD-06EE-4465-90EC-C77A36FB1B23}" 29 | EndProject 30 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada.Extensions.Cors", "extensions\Ntrada.Extensions.Cors\Ntrada.Extensions.Cors.csproj", "{29689A6F-5460-4100-B51A-78EF322DF5D5}" 31 | EndProject 32 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada.Extensions.Tracing", "extensions\Ntrada.Extensions.Tracing\Ntrada.Extensions.Tracing.csproj", "{CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}" 33 | EndProject 34 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada.Samples.Services.Orders", "samples\Ntrada.Samples.Services.Orders\Ntrada.Samples.Services.Orders.csproj", "{790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}" 35 | EndProject 36 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ntrada.Extensions.Swagger", "extensions\Ntrada.Extensions.Swagger\Ntrada.Extensions.Swagger.csproj", "{63C2B069-50D1-46DD-96CF-B3C2A325A3A8}" 37 | EndProject 38 | Global 39 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 40 | Debug|Any CPU = Debug|Any CPU 41 | Debug|x64 = Debug|x64 42 | Debug|x86 = Debug|x86 43 | Release|Any CPU = Release|Any CPU 44 | Release|x64 = Release|x64 45 | Release|x86 = Release|x86 46 | EndGlobalSection 47 | GlobalSection(SolutionProperties) = preSolution 48 | HideSolutionNode = FALSE 49 | EndGlobalSection 50 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 51 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Debug|x64.ActiveCfg = Debug|Any CPU 54 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Debug|x64.Build.0 = Debug|Any CPU 55 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Debug|x86.ActiveCfg = Debug|Any CPU 56 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Debug|x86.Build.0 = Debug|Any CPU 57 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Release|x64.ActiveCfg = Release|Any CPU 60 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Release|x64.Build.0 = Release|Any CPU 61 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Release|x86.ActiveCfg = Release|Any CPU 62 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC}.Release|x86.Build.0 = Release|Any CPU 63 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 64 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Debug|Any CPU.Build.0 = Debug|Any CPU 65 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Debug|x64.ActiveCfg = Debug|Any CPU 66 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Debug|x64.Build.0 = Debug|Any CPU 67 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Debug|x86.ActiveCfg = Debug|Any CPU 68 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Debug|x86.Build.0 = Debug|Any CPU 69 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Release|Any CPU.ActiveCfg = Release|Any CPU 70 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Release|Any CPU.Build.0 = Release|Any CPU 71 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Release|x64.ActiveCfg = Release|Any CPU 72 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Release|x64.Build.0 = Release|Any CPU 73 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Release|x86.ActiveCfg = Release|Any CPU 74 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0}.Release|x86.Build.0 = Release|Any CPU 75 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 76 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Debug|Any CPU.Build.0 = Debug|Any CPU 77 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Debug|x64.ActiveCfg = Debug|Any CPU 78 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Debug|x64.Build.0 = Debug|Any CPU 79 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Debug|x86.ActiveCfg = Debug|Any CPU 80 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Debug|x86.Build.0 = Debug|Any CPU 81 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Release|Any CPU.ActiveCfg = Release|Any CPU 82 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Release|Any CPU.Build.0 = Release|Any CPU 83 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Release|x64.ActiveCfg = Release|Any CPU 84 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Release|x64.Build.0 = Release|Any CPU 85 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Release|x86.ActiveCfg = Release|Any CPU 86 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A}.Release|x86.Build.0 = Release|Any CPU 87 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 88 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Debug|Any CPU.Build.0 = Debug|Any CPU 89 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Debug|x64.ActiveCfg = Debug|Any CPU 90 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Debug|x64.Build.0 = Debug|Any CPU 91 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Debug|x86.ActiveCfg = Debug|Any CPU 92 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Debug|x86.Build.0 = Debug|Any CPU 93 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Release|Any CPU.ActiveCfg = Release|Any CPU 94 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Release|Any CPU.Build.0 = Release|Any CPU 95 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Release|x64.ActiveCfg = Release|Any CPU 96 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Release|x64.Build.0 = Release|Any CPU 97 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Release|x86.ActiveCfg = Release|Any CPU 98 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C}.Release|x86.Build.0 = Release|Any CPU 99 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 100 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Debug|Any CPU.Build.0 = Debug|Any CPU 101 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Debug|x64.ActiveCfg = Debug|Any CPU 102 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Debug|x64.Build.0 = Debug|Any CPU 103 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Debug|x86.ActiveCfg = Debug|Any CPU 104 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Debug|x86.Build.0 = Debug|Any CPU 105 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Release|Any CPU.ActiveCfg = Release|Any CPU 106 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Release|Any CPU.Build.0 = Release|Any CPU 107 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Release|x64.ActiveCfg = Release|Any CPU 108 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Release|x64.Build.0 = Release|Any CPU 109 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Release|x86.ActiveCfg = Release|Any CPU 110 | {5195DD55-8257-4238-9B92-33E8574BBAA0}.Release|x86.Build.0 = Release|Any CPU 111 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 112 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Debug|Any CPU.Build.0 = Debug|Any CPU 113 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Debug|x64.ActiveCfg = Debug|Any CPU 114 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Debug|x64.Build.0 = Debug|Any CPU 115 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Debug|x86.ActiveCfg = Debug|Any CPU 116 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Debug|x86.Build.0 = Debug|Any CPU 117 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Release|Any CPU.ActiveCfg = Release|Any CPU 118 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Release|Any CPU.Build.0 = Release|Any CPU 119 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Release|x64.ActiveCfg = Release|Any CPU 120 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Release|x64.Build.0 = Release|Any CPU 121 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Release|x86.ActiveCfg = Release|Any CPU 122 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536}.Release|x86.Build.0 = Release|Any CPU 123 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 124 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Debug|Any CPU.Build.0 = Debug|Any CPU 125 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Debug|x64.ActiveCfg = Debug|Any CPU 126 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Debug|x64.Build.0 = Debug|Any CPU 127 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Debug|x86.ActiveCfg = Debug|Any CPU 128 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Debug|x86.Build.0 = Debug|Any CPU 129 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Release|Any CPU.ActiveCfg = Release|Any CPU 130 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Release|Any CPU.Build.0 = Release|Any CPU 131 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Release|x64.ActiveCfg = Release|Any CPU 132 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Release|x64.Build.0 = Release|Any CPU 133 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Release|x86.ActiveCfg = Release|Any CPU 134 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402}.Release|x86.Build.0 = Release|Any CPU 135 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 136 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Debug|Any CPU.Build.0 = Debug|Any CPU 137 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Debug|x64.ActiveCfg = Debug|Any CPU 138 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Debug|x64.Build.0 = Debug|Any CPU 139 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Debug|x86.ActiveCfg = Debug|Any CPU 140 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Debug|x86.Build.0 = Debug|Any CPU 141 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Release|Any CPU.ActiveCfg = Release|Any CPU 142 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Release|Any CPU.Build.0 = Release|Any CPU 143 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Release|x64.ActiveCfg = Release|Any CPU 144 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Release|x64.Build.0 = Release|Any CPU 145 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Release|x86.ActiveCfg = Release|Any CPU 146 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23}.Release|x86.Build.0 = Release|Any CPU 147 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 148 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Debug|Any CPU.Build.0 = Debug|Any CPU 149 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Debug|x64.ActiveCfg = Debug|Any CPU 150 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Debug|x64.Build.0 = Debug|Any CPU 151 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Debug|x86.ActiveCfg = Debug|Any CPU 152 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Debug|x86.Build.0 = Debug|Any CPU 153 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Release|Any CPU.ActiveCfg = Release|Any CPU 154 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Release|Any CPU.Build.0 = Release|Any CPU 155 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Release|x64.ActiveCfg = Release|Any CPU 156 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Release|x64.Build.0 = Release|Any CPU 157 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Release|x86.ActiveCfg = Release|Any CPU 158 | {29689A6F-5460-4100-B51A-78EF322DF5D5}.Release|x86.Build.0 = Release|Any CPU 159 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 160 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 161 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Debug|x64.ActiveCfg = Debug|Any CPU 162 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Debug|x64.Build.0 = Debug|Any CPU 163 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Debug|x86.ActiveCfg = Debug|Any CPU 164 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Debug|x86.Build.0 = Debug|Any CPU 165 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 166 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Release|Any CPU.Build.0 = Release|Any CPU 167 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Release|x64.ActiveCfg = Release|Any CPU 168 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Release|x64.Build.0 = Release|Any CPU 169 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Release|x86.ActiveCfg = Release|Any CPU 170 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF}.Release|x86.Build.0 = Release|Any CPU 171 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 172 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Debug|Any CPU.Build.0 = Debug|Any CPU 173 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Debug|x64.ActiveCfg = Debug|Any CPU 174 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Debug|x64.Build.0 = Debug|Any CPU 175 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Debug|x86.ActiveCfg = Debug|Any CPU 176 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Debug|x86.Build.0 = Debug|Any CPU 177 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Release|Any CPU.ActiveCfg = Release|Any CPU 178 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Release|Any CPU.Build.0 = Release|Any CPU 179 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Release|x64.ActiveCfg = Release|Any CPU 180 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Release|x64.Build.0 = Release|Any CPU 181 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Release|x86.ActiveCfg = Release|Any CPU 182 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE}.Release|x86.Build.0 = Release|Any CPU 183 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 184 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Debug|Any CPU.Build.0 = Debug|Any CPU 185 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Debug|x64.ActiveCfg = Debug|Any CPU 186 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Debug|x64.Build.0 = Debug|Any CPU 187 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Debug|x86.ActiveCfg = Debug|Any CPU 188 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Debug|x86.Build.0 = Debug|Any CPU 189 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Release|Any CPU.ActiveCfg = Release|Any CPU 190 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Release|Any CPU.Build.0 = Release|Any CPU 191 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Release|x64.ActiveCfg = Release|Any CPU 192 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Release|x64.Build.0 = Release|Any CPU 193 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Release|x86.ActiveCfg = Release|Any CPU 194 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8}.Release|x86.Build.0 = Release|Any CPU 195 | EndGlobalSection 196 | GlobalSection(NestedProjects) = preSolution 197 | {BD735FBA-1D9C-45AD-8BDD-1E89B059E4CC} = {10BC7168-BE16-4A3C-A7CC-F122C83E08F5} 198 | {0B949CC3-A1AF-4460-A942-D03ADA500BC0} = {10BC7168-BE16-4A3C-A7CC-F122C83E08F5} 199 | {8A8B41A2-9EAB-4E7C-9BDA-2DB93636CD2A} = {D1922E6F-C1D9-4C38-B667-36E417734DE8} 200 | {254EE5DD-ACA8-4BE0-B8DD-5594C2C03D6C} = {D1922E6F-C1D9-4C38-B667-36E417734DE8} 201 | {5195DD55-8257-4238-9B92-33E8574BBAA0} = {367233AC-1E0D-4FA2-8701-09702663DD54} 202 | {B606AE04-E5B7-4912-AEB1-CCEDD3CBA536} = {6E8C9C7B-7249-41CD-A466-C11A8644400B} 203 | {4C88EF7A-90AC-4E3E-A819-2CC4CCC17402} = {6E8C9C7B-7249-41CD-A466-C11A8644400B} 204 | {AB8D52BD-06EE-4465-90EC-C77A36FB1B23} = {6E8C9C7B-7249-41CD-A466-C11A8644400B} 205 | {29689A6F-5460-4100-B51A-78EF322DF5D5} = {6E8C9C7B-7249-41CD-A466-C11A8644400B} 206 | {CC9A3D1E-84DC-43A2-9BC1-BBFE0DF471AF} = {6E8C9C7B-7249-41CD-A466-C11A8644400B} 207 | {790DE86C-8DF8-4476-BC2E-C7836CBBA1CE} = {367233AC-1E0D-4FA2-8701-09702663DD54} 208 | {63C2B069-50D1-46DD-96CF-B3C2A325A3A8} = {6E8C9C7B-7249-41CD-A466-C11A8644400B} 209 | EndGlobalSection 210 | EndGlobal 211 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | / | / / /__________ _____/ /___ _ 3 | / |/ / __/ ___/ __ `/ __ / __ `/ 4 | / /| / /_/ / / /_/ / /_/ / /_/ / 5 | /_/ |_/\__/_/ \__,_/\__,_/\__,_/ 6 | 7 | /___ API Gateway (Entrance) ___/ 8 | ``` 9 | ---------------- 10 | 11 | 12 | **`Ntrada`** (*entrada* is a spanish word meaning an entrance). 13 | 14 | The aim of this project is to provide an easily configurable (via YML) and extendable (e.g. RabbitMQ integration, OpenTracing etc.) API Gateway, that requires no coding whatsoever and can be started via Docker or as .NET Core application. 15 | 16 | ### Features: 17 | 18 | * Configuration via single file 19 | * Separate modules definitions 20 | * Static content 21 | * Routing 22 | * Match-all methods generic templates 23 | * Request forwarding 24 | * Headers forwarding 25 | * Custom request bodies 26 | * Request body validation 27 | * Query string binding 28 | * Request & response headers modification 29 | * Basic request & response transformation 30 | * Authentication 31 | * Authorization 32 | * HTTP retries 33 | * Strongly-typed service names 34 | * Extensibility with custom request handlers 35 | 36 | ### Extensions: 37 | 38 | * JWT 39 | * RabbitMQ integration 40 | * Open Tracing with Jaeger 41 | * CORS 42 | * Custom errors 43 | 44 | 45 | No documentation yet, please take a look at the basic [ntrada.yml](https://github.com/snatch-dev/Ntrada/blob/master/samples/Ntrada.Samples.Api/ntrada.yml) configuration. 46 | 47 | ```yml 48 | modules: 49 | - name: home 50 | routes: 51 | - upstream: / 52 | method: GET 53 | use: return_value 54 | return_value: Welcome to Ntrada API. 55 | ``` 56 | 57 | Start via Docker: 58 | 59 | ``` 60 | docker build -t ntrada . 61 | docker run -it --rm --name ntrada -p 5000:80 ntrada 62 | curl localhost:5000 63 | ``` 64 | 65 | Or as .NET Core application: 66 | 67 | ``` 68 | cd src/Ntrada.Host/ 69 | dotnet run 70 | curl localhost:5000 71 | ``` 72 | 73 | If you're willing to create your own application (instead of running it via Docker), it's all it takes to use Ntrada: 74 | 75 | ```csharp 76 | public class Program 77 | { 78 | public static Task Main(string[] args) 79 | => CreateHostBuilder(args).Build().RunAsync(); 80 | 81 | public static IHostBuilder CreateHostBuilder(string[] args) => 82 | Host.CreateDefaultBuilder(args) 83 | .ConfigureWebHostDefaults(webBuilder => 84 | { 85 | webBuilder.ConfigureAppConfiguration(builder => 86 | { 87 | var configPath = args?.FirstOrDefault() ?? "ntrada.yml"; 88 | builder.AddYamlFile(configPath, false); 89 | }).UseStartup(); 90 | }); 91 | } 92 | ``` 93 | 94 | More **real-world examples** (modules, asynchronous messaging, load balancing etc.) can be found in the following projects: 95 | 96 | * [Pacco](https://github.com/devmentors/Pacco.APIGateway) 97 | 98 | ---------------- 99 | 100 | **Advanced configuration** 101 | 102 | ```yml 103 | auth: 104 | enabled: true 105 | global: false 106 | claims: 107 | role: http://schemas.microsoft.com/ws/2008/06/identity/claims/role 108 | 109 | http: 110 | retries: 2 111 | interval: 2.0 112 | exponential: true 113 | 114 | useErrorHandler: true 115 | useJaeger: true 116 | useForwardedHeaders: true 117 | passQueryString: true 118 | modulesPath: Modules 119 | payloadsFolder: Payloads 120 | forwardRequestHeaders: true 121 | forwardResponseHeaders: true 122 | generateRequestId: true 123 | generateTraceId: true 124 | resourceId: 125 | generate: true 126 | property: id 127 | useLocalUrl: true 128 | loadBalancer: 129 | enabled: false 130 | url: localhost:9999 131 | 132 | extensions: 133 | customErrors: 134 | includeExceptionMessage: true 135 | 136 | cors: 137 | allowCredentials: true 138 | allowedOrigins: 139 | - '*' 140 | allowedMethods: 141 | - post 142 | - delete 143 | allowedHeaders: 144 | - '*' 145 | exposedHeaders: 146 | - Request-ID 147 | - Resource-ID 148 | - Trace-ID 149 | - Total-Count 150 | 151 | jwt: 152 | key: eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij 153 | issuer: ntrada 154 | issuers: 155 | validateIssuer: true 156 | audience: 157 | audiences: 158 | validateAudience: false 159 | validateLifetime: true 160 | 161 | rabbitmq: 162 | enabled: true 163 | connectionName: ntrada 164 | hostnames: 165 | - localhost 166 | port: 5672 167 | virtualHost: / 168 | username: guest 169 | password: guest 170 | requestedConnectionTimeout: 3000 171 | socketReadTimeout: 3000 172 | socketWriteTimeout: 3000 173 | requestedHeartbeat: 60 174 | exchange: 175 | declareExchange: true 176 | durable: true 177 | autoDelete: false 178 | type: topic 179 | messageContext: 180 | enabled: true 181 | header: message_context 182 | logger: 183 | enabled: true 184 | spanContextHeader: span_context 185 | 186 | tracing: 187 | serviceName: ntrada 188 | udpHost: localhost 189 | udpPort: 6831 190 | maxPacketSize: 0 191 | sampler: const 192 | useEmptyTracer: false 193 | 194 | modules: 195 | home: 196 | routes: 197 | - upstream: / 198 | method: GET 199 | use: return_value 200 | returnValue: Welcome to Ntrada API! 201 | 202 | - upstream: / 203 | method: POST 204 | auth: false 205 | use: rabbitmq 206 | config: 207 | exchange: sample.exchange 208 | routing_key: sample.routing.key 209 | 210 | orders: 211 | routes: 212 | - upstream: /orders 213 | methods: 214 | - GET 215 | - POST 216 | - DELETE 217 | matchAll: true 218 | use: downstream 219 | downstream: orders-service/orders 220 | 221 | services: 222 | orders-service: 223 | localUrl: localhost:5001 224 | url: orders-service 225 | ``` -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Cors/CorsExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Ntrada.Extensions.Cors 6 | { 7 | public class CorsExtension : IExtension 8 | { 9 | public string Name => "cors"; 10 | public string Description => "Cross-Origin Resource Sharing"; 11 | 12 | public void Add(IServiceCollection services, IOptionsProvider optionsProvider) 13 | { 14 | var options = optionsProvider.GetForExtension(Name); 15 | services.AddCors(cors => 16 | { 17 | var allowedHeaders = options.AllowedHeaders ?? Enumerable.Empty(); 18 | var allowedMethods = options.AllowedMethods ?? Enumerable.Empty(); 19 | var allowedOrigins = options.AllowedOrigins ?? Enumerable.Empty(); 20 | var exposedHeaders = options.ExposedHeaders ?? Enumerable.Empty(); 21 | cors.AddPolicy("CorsPolicy", builder => 22 | { 23 | var origins = allowedOrigins.ToArray(); 24 | if (options.AllowCredentials && origins.FirstOrDefault() != "*") 25 | { 26 | builder.AllowCredentials(); 27 | } 28 | else 29 | { 30 | builder.DisallowCredentials(); 31 | } 32 | 33 | builder.WithHeaders(allowedHeaders.ToArray()) 34 | .WithMethods(allowedMethods.ToArray()) 35 | .WithOrigins(origins.ToArray()) 36 | .WithExposedHeaders(exposedHeaders.ToArray()); 37 | }); 38 | }); 39 | } 40 | 41 | public void Use(IApplicationBuilder app, IOptionsProvider optionsProvider) 42 | { 43 | app.UseCors("CorsPolicy"); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Cors/CorsOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada.Extensions.Cors 4 | { 5 | public class CorsOptions : IOptions 6 | { 7 | public bool AllowCredentials { get; set; } 8 | public IEnumerable AllowedOrigins { get; set; } 9 | public IEnumerable AllowedMethods { get; set; } 10 | public IEnumerable AllowedHeaders { get; set; } 11 | public IEnumerable ExposedHeaders { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Cors/Ntrada.Extensions.Cors.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Cors/cors.yml: -------------------------------------------------------------------------------- 1 | cors: 2 | allowCredentials: true 3 | allowedOrigins: 4 | - '*' 5 | allowedMethods: 6 | - post 7 | - delete 8 | allowedHeaders: 9 | - '*' 10 | exposedHeaders: 11 | - Request-ID 12 | - Resource-ID 13 | - Trace-ID 14 | - Total-Count -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.CustomErrors/CustomErrorsExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Ntrada.Extensions.CustomErrors 5 | { 6 | public class CustomErrorsExtension : IExtension 7 | { 8 | public string Name => "customErrors"; 9 | public string Description => "Custom errors handler"; 10 | 11 | public void Add(IServiceCollection services, IOptionsProvider optionsProvider) 12 | { 13 | var options = optionsProvider.GetForExtension(Name); 14 | services.AddSingleton(options); 15 | services.AddScoped(); 16 | } 17 | 18 | public void Use(IApplicationBuilder app, IOptionsProvider optionsProvider) 19 | { 20 | app.UseMiddleware(); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.CustomErrors/CustomErrorsOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Extensions.CustomErrors 2 | { 3 | public class CustomErrorsOptions : IOptions 4 | { 5 | public bool IncludeExceptionMessage { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.CustomErrors/ErrorHandlerMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Logging; 6 | using Newtonsoft.Json; 7 | 8 | namespace Ntrada.Extensions.CustomErrors 9 | { 10 | public class ErrorHandlerMiddleware : IMiddleware 11 | { 12 | private readonly CustomErrorsOptions _options; 13 | private readonly ILogger _logger; 14 | 15 | public ErrorHandlerMiddleware(CustomErrorsOptions options, ILogger logger) 16 | { 17 | _options = options; 18 | _logger = logger; 19 | } 20 | 21 | public async Task InvokeAsync(HttpContext context, RequestDelegate next) 22 | { 23 | try 24 | { 25 | await next(context); 26 | } 27 | catch (Exception exception) 28 | { 29 | _logger.LogError(exception, exception.Message); 30 | await HandleErrorAsync(context, exception); 31 | } 32 | } 33 | 34 | private Task HandleErrorAsync(HttpContext context, Exception exception) 35 | { 36 | var errorCode = "error"; 37 | var statusCode = HttpStatusCode.BadRequest; 38 | var message = _options.IncludeExceptionMessage ? exception.Message : "There was an error."; 39 | var response = new 40 | { 41 | errors = new[] 42 | { 43 | new Error 44 | { 45 | Code = errorCode, 46 | Message = message, 47 | } 48 | } 49 | }; 50 | var payload = JsonConvert.SerializeObject(response); 51 | context.Response.ContentType = "application/json"; 52 | context.Response.StatusCode = (int) statusCode; 53 | 54 | return context.Response.WriteAsync(payload); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.CustomErrors/Ntrada.Extensions.CustomErrors.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.CustomErrors/customErrors.yml: -------------------------------------------------------------------------------- 1 | customErrors: 2 | includeExceptionMessage: true -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Jwt/JwtExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Microsoft.AspNetCore.Authentication.JwtBearer; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.IdentityModel.Tokens; 7 | 8 | namespace Ntrada.Extensions.Jwt 9 | { 10 | internal class JwtExtension : IExtension 11 | { 12 | public string Name => "jwt"; 13 | public string Description => "JSON Web Token authentication"; 14 | 15 | public void Add(IServiceCollection services, IOptionsProvider optionsProvider) 16 | { 17 | var options = optionsProvider.GetForExtension(Name); 18 | services.AddAuthorization(); 19 | 20 | var tokenValidationParameters = new TokenValidationParameters 21 | { 22 | RequireAudience = options.RequireAudience, 23 | ValidIssuer = options.ValidIssuer, 24 | ValidIssuers = options.ValidIssuers, 25 | ValidateActor = options.ValidateActor, 26 | ValidAudience = options.ValidAudience, 27 | ValidAudiences = options.ValidAudiences, 28 | ValidateAudience = options.ValidateAudience, 29 | ValidateIssuer = options.ValidateIssuer, 30 | ValidateLifetime = options.ValidateLifetime, 31 | ValidateTokenReplay = options.ValidateTokenReplay, 32 | ValidateIssuerSigningKey = options.ValidateIssuerSigningKey, 33 | SaveSigninToken = options.SaveSigninToken, 34 | RequireExpirationTime = options.RequireExpirationTime, 35 | RequireSignedTokens = options.RequireSignedTokens, 36 | ClockSkew = TimeSpan.Zero 37 | }; 38 | 39 | if (!string.IsNullOrWhiteSpace(options.AuthenticationType)) 40 | { 41 | tokenValidationParameters.AuthenticationType = options.AuthenticationType; 42 | } 43 | 44 | if (!string.IsNullOrWhiteSpace(options.IssuerSigningKey)) 45 | { 46 | tokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey( 47 | Encoding.UTF8.GetBytes(options.IssuerSigningKey)); 48 | } 49 | 50 | if (!string.IsNullOrWhiteSpace(options.NameClaimType)) 51 | { 52 | tokenValidationParameters.NameClaimType = options.NameClaimType; 53 | } 54 | 55 | if (!string.IsNullOrWhiteSpace(options.RoleClaimType)) 56 | { 57 | tokenValidationParameters.RoleClaimType = options.RoleClaimType; 58 | } 59 | 60 | services.AddSingleton(tokenValidationParameters); 61 | 62 | services.AddAuthentication(o => 63 | { 64 | o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 65 | o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 66 | }) 67 | .AddJwtBearer(o => 68 | { 69 | o.Authority = options.Authority; 70 | o.Audience = options.Audience; 71 | o.MetadataAddress = options.MetadataAddress; 72 | o.SaveToken = options.SaveToken; 73 | o.RefreshOnIssuerKeyNotFound = options.RefreshOnIssuerKeyNotFound; 74 | o.RequireHttpsMetadata = options.RequireHttpsMetadata; 75 | o.IncludeErrorDetails = options.IncludeErrorDetails; 76 | o.TokenValidationParameters = tokenValidationParameters; 77 | if (!string.IsNullOrWhiteSpace(options.Challenge)) 78 | { 79 | o.Challenge = options.Challenge; 80 | } 81 | }); 82 | } 83 | 84 | public void Use(IApplicationBuilder app, IOptionsProvider optionsProvider) 85 | { 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Jwt/JwtOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada.Extensions.Jwt 4 | { 5 | internal class JwtOptions : IOptions 6 | { 7 | public string IssuerSigningKey { get; set; } 8 | public string Authority { get; set; } 9 | public string Audience { get; set; } 10 | public string Challenge { get; set; } = "Bearer"; 11 | public string MetadataAddress { get; set; } 12 | public bool SaveToken { get; set; } = true; 13 | public bool SaveSigninToken { get; set; } 14 | public bool RequireAudience { get; set; } = true; 15 | public bool RequireHttpsMetadata { get; set; } = true; 16 | public bool RequireExpirationTime { get; set; } = true; 17 | public bool RequireSignedTokens { get; set; } = true; 18 | public string ValidAudience { get; set; } 19 | public IEnumerable ValidAudiences { get; set; } 20 | public string ValidIssuer { get; set; } 21 | public IEnumerable ValidIssuers { get; set; } 22 | public bool ValidateActor { get; set; } 23 | public bool ValidateAudience { get; set; } = true; 24 | public bool ValidateIssuer { get; set; } = true; 25 | public bool ValidateLifetime { get; set; } = true; 26 | public bool ValidateTokenReplay { get; set; } 27 | public bool ValidateIssuerSigningKey { get; set; } 28 | public bool RefreshOnIssuerKeyNotFound { get; set; } = true; 29 | public bool IncludeErrorDetails { get; set; } = true; 30 | public string AuthenticationType { get; set; } 31 | public string NameClaimType { get; set; } 32 | public string RoleClaimType { get; set; } 33 | } 34 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Jwt/Ntrada.Extensions.Jwt.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Jwt/jwt.yml: -------------------------------------------------------------------------------- 1 | jwt: 2 | issuerSigningKey: eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij 3 | validIssuer: ntrada 4 | validateAudience: false 5 | validateIssuer: true 6 | validateLifetime: true -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/Clients/RabbitMqClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Microsoft.Extensions.Logging; 5 | using Newtonsoft.Json; 6 | using RabbitMQ.Client; 7 | 8 | namespace Ntrada.Extensions.RabbitMq.Clients 9 | { 10 | internal sealed class RabbitMqClient : IRabbitMqClient 11 | { 12 | private const string EmptyContext = "{}"; 13 | private readonly IConnection _connection; 14 | private readonly ILogger _logger; 15 | private readonly string _messageContextHeader; 16 | private readonly bool _messageContextEnabled; 17 | private readonly bool _loggerEnabled; 18 | private readonly string _spanContextHeader; 19 | 20 | public RabbitMqClient(IConnection connection, RabbitMqOptions options, ILogger logger) 21 | { 22 | _connection = connection; 23 | _logger = logger; 24 | _messageContextEnabled = options.MessageContext?.Enabled == true; 25 | _messageContextHeader = string.IsNullOrWhiteSpace(options.MessageContext?.Header) 26 | ? "message_context" 27 | : options.MessageContext.Header; 28 | _loggerEnabled = options.Logger?.Enabled == true; 29 | _spanContextHeader = string.IsNullOrWhiteSpace(options.SpanContextHeader) 30 | ? "span_context" 31 | : options.SpanContextHeader; 32 | } 33 | 34 | public void Send(object message, string routingKey, string exchange, string messageId = null, 35 | string correlationId = null, string spanContext = null, object messageContext = null, 36 | IDictionary headers = null) 37 | { 38 | using var channel = _connection.CreateModel(); 39 | var json = JsonConvert.SerializeObject(message); 40 | var body = Encoding.UTF8.GetBytes(json); 41 | var properties = channel.CreateBasicProperties(); 42 | properties.MessageId = string.IsNullOrWhiteSpace(messageId) 43 | ? Guid.NewGuid().ToString("N") 44 | : messageId; 45 | properties.CorrelationId = string.IsNullOrWhiteSpace(correlationId) 46 | ? Guid.NewGuid().ToString("N") 47 | : correlationId; 48 | properties.Timestamp = new AmqpTimestamp(DateTimeOffset.UtcNow.ToUnixTimeSeconds()); 49 | properties.Headers = new Dictionary(); 50 | if (_messageContextEnabled) 51 | { 52 | IncludeMessageContext(messageContext, properties); 53 | } 54 | 55 | if (!string.IsNullOrWhiteSpace(spanContext)) 56 | { 57 | properties.Headers.Add(_spanContextHeader, spanContext); 58 | } 59 | 60 | if (headers is {}) 61 | { 62 | foreach (var (key, value) in headers) 63 | { 64 | if (string.IsNullOrWhiteSpace(key) || value is null) 65 | { 66 | continue; 67 | } 68 | 69 | properties.Headers.TryAdd(key, value); 70 | } 71 | } 72 | 73 | if (_loggerEnabled) 74 | { 75 | _logger.LogInformation($"Sending a message with routing key: '{routingKey}' to the exchange: " + 76 | $"'{exchange}' [message id: '{properties.MessageId}', correlation id: '{properties.CorrelationId}']."); 77 | } 78 | 79 | channel.BasicPublish(exchange, routingKey, properties, body); 80 | } 81 | 82 | private void IncludeMessageContext(object context, IBasicProperties properties) 83 | { 84 | if (context is {}) 85 | { 86 | properties.Headers.Add(_messageContextHeader, JsonConvert.SerializeObject(context)); 87 | return; 88 | } 89 | 90 | properties.Headers.Add(_messageContextHeader, EmptyContext); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/Contexts/NullContext.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Extensions.RabbitMq.Contexts 2 | { 3 | internal sealed class NullContext 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/Contexts/NullContextBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Extensions.RabbitMq.Contexts 2 | { 3 | internal sealed class NullContextBuilder : IContextBuilder 4 | { 5 | public object Build(ExecutionData executionData) => null; 6 | } 7 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/Contexts/NullSpanContextBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Extensions.RabbitMq.Contexts 2 | { 3 | internal sealed class NullSpanContextBuilder : ISpanContextBuilder 4 | { 5 | public string Build(ExecutionData executionData) => null; 6 | } 7 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/Handlers/RabbitMqHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Ntrada.Hooks; 7 | using Route = Ntrada.Configuration.Route; 8 | 9 | namespace Ntrada.Extensions.RabbitMq.Handlers 10 | { 11 | internal sealed class RabbitMqHandler : IHandler 12 | { 13 | private const string RequestIdHeader = "Request-ID"; 14 | private const string ResourceIdHeader = "Resource-ID"; 15 | private const string TraceIdHeader = "Trace-ID"; 16 | private const string ConfigRoutingKey = "routing_key"; 17 | private const string ConfigExchange = "exchange"; 18 | private readonly IRabbitMqClient _rabbitMqClient; 19 | private readonly IRequestProcessor _requestProcessor; 20 | private readonly IPayloadBuilder _payloadBuilder; 21 | private readonly IPayloadValidator _payloadValidator; 22 | private readonly RabbitMqOptions _options; 23 | private readonly IContextBuilder _contextBuilder; 24 | private readonly ISpanContextBuilder _spanContextBuilder; 25 | private readonly IEnumerable _requestHooks; 26 | private readonly IEnumerable _responseHooks; 27 | 28 | public RabbitMqHandler(IRabbitMqClient rabbitMqClient, IContextBuilder contextBuilder, 29 | ISpanContextBuilder spanContextBuilder, IRequestProcessor requestProcessor, IPayloadBuilder payloadBuilder, 30 | IPayloadValidator payloadValidator, RabbitMqOptions options, IServiceProvider serviceProvider) 31 | { 32 | _rabbitMqClient = rabbitMqClient; 33 | _contextBuilder = contextBuilder; 34 | _spanContextBuilder = spanContextBuilder; 35 | _requestProcessor = requestProcessor; 36 | _payloadBuilder = payloadBuilder; 37 | _payloadValidator = payloadValidator; 38 | _options = options; 39 | _requestHooks = serviceProvider.GetServices(); 40 | _responseHooks = serviceProvider.GetServices(); 41 | } 42 | 43 | public string GetInfo(Route route) => $"send a message to the exchange: '{route.Config["routing_key"]}'"; 44 | 45 | public async Task HandleAsync(HttpContext context, RouteConfig config) 46 | { 47 | var executionData = await _requestProcessor.ProcessAsync(config, context); 48 | if (_requestHooks is {}) 49 | { 50 | foreach (var hook in _requestHooks) 51 | { 52 | if (hook is null) 53 | { 54 | continue; 55 | } 56 | 57 | await hook.InvokeAsync(context.Request, executionData); 58 | } 59 | } 60 | 61 | if (!executionData.IsPayloadValid) 62 | { 63 | await _payloadValidator.TryValidate(executionData, context.Response); 64 | return; 65 | } 66 | 67 | var traceId = context.TraceIdentifier; 68 | var routeConfig = executionData.Route.Config; 69 | var routingKey = routeConfig[ConfigRoutingKey]; 70 | var exchange = routeConfig[ConfigExchange]; 71 | var message = executionData.HasPayload 72 | ? executionData.Payload 73 | : await _payloadBuilder.BuildJsonAsync(context.Request); 74 | var messageContext = _contextBuilder.Build(executionData); 75 | var hasTraceId = !string.IsNullOrWhiteSpace(traceId); 76 | var spanContext = _spanContextBuilder.Build(executionData); 77 | var messageId = Guid.NewGuid().ToString("N"); 78 | var correlationId = executionData.RequestId; 79 | 80 | _rabbitMqClient.Send(message, routingKey, exchange, messageId, correlationId, spanContext, 81 | messageContext, _options.Headers); 82 | 83 | if (!string.IsNullOrWhiteSpace(executionData.RequestId)) 84 | { 85 | context.Response.Headers.Add(RequestIdHeader, executionData.RequestId); 86 | } 87 | 88 | if (!string.IsNullOrWhiteSpace(executionData.ResourceId) && context.Request.Method is "POST") 89 | { 90 | context.Response.Headers.Add(ResourceIdHeader, executionData.ResourceId); 91 | } 92 | 93 | if (hasTraceId) 94 | { 95 | context.Response.Headers.Add(TraceIdHeader, traceId); 96 | } 97 | 98 | if (_responseHooks is {}) 99 | { 100 | foreach (var hook in _responseHooks) 101 | { 102 | if (hook is null) 103 | { 104 | continue; 105 | } 106 | 107 | await hook.InvokeAsync(context.Response, executionData); 108 | } 109 | } 110 | 111 | context.Response.StatusCode = 202; 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/IContextBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Extensions.RabbitMq 2 | { 3 | public interface IContextBuilder 4 | { 5 | object Build(ExecutionData executionData); 6 | } 7 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/IRabbitMqClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada.Extensions.RabbitMq 4 | { 5 | public interface IRabbitMqClient 6 | { 7 | void Send(object message, string routingKey, string exchange, string messageId = null, 8 | string correlationId = null, string spanContext = null, object messageContext = null, 9 | IDictionary headers = null); 10 | } 11 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/ISpanContextBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Extensions.RabbitMq 2 | { 3 | public interface ISpanContextBuilder 4 | { 5 | string Build(ExecutionData executionData); 6 | } 7 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/Ntrada.Extensions.RabbitMq.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/RabbitMqExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | using Ntrada.Extensions.RabbitMq.Clients; 7 | using Ntrada.Extensions.RabbitMq.Contexts; 8 | using Ntrada.Extensions.RabbitMq.Handlers; 9 | using Ntrada.Options; 10 | using RabbitMQ.Client; 11 | 12 | namespace Ntrada.Extensions.RabbitMq 13 | { 14 | public class RabbitMqExtension : IExtension 15 | { 16 | public string Name => "rabbitmq"; 17 | public string Description => "RabbitMQ message broker"; 18 | 19 | public void Add(IServiceCollection services, IOptionsProvider optionsProvider) 20 | { 21 | var options = optionsProvider.GetForExtension(Name); 22 | services.AddSingleton(options); 23 | services.AddSingleton(sp => 24 | { 25 | var connectionFactory = new ConnectionFactory 26 | { 27 | HostName = options.Hostnames?.FirstOrDefault(), 28 | Port = options.Port, 29 | VirtualHost = options.VirtualHost, 30 | UserName = options.Username, 31 | Password = options.Password, 32 | RequestedConnectionTimeout = options.RequestedConnectionTimeout, 33 | SocketReadTimeout = options.SocketReadTimeout, 34 | SocketWriteTimeout = options.SocketWriteTimeout, 35 | RequestedChannelMax = options.RequestedChannelMax, 36 | RequestedFrameMax = options.RequestedFrameMax, 37 | RequestedHeartbeat = options.RequestedHeartbeat, 38 | UseBackgroundThreadsForIO = options.UseBackgroundThreadsForIO, 39 | Ssl = options.Ssl is null 40 | ? new SslOption() 41 | : new SslOption(options.Ssl.ServerName, options.Ssl.CertificatePath, options.Ssl.Enabled), 42 | }; 43 | 44 | var connection = connectionFactory.CreateConnection(options.ConnectionName); 45 | if (options.Exchange?.DeclareExchange != true) 46 | { 47 | return connection; 48 | } 49 | 50 | var ntradaOptions = optionsProvider.Get(); 51 | var exchanges = ntradaOptions.Modules 52 | .SelectMany(m => m.Value.Routes) 53 | .Where(m => m.Use.Equals(Name, StringComparison.InvariantCultureIgnoreCase)) 54 | .SelectMany(r => r.Config) 55 | .Where(c => c.Key.Equals("exchange", StringComparison.InvariantCultureIgnoreCase)) 56 | .Distinct() 57 | .ToList(); 58 | 59 | if (!exchanges.Any()) 60 | { 61 | return connection; 62 | } 63 | 64 | var logger = sp.GetService>(); 65 | var loggerEnabled = options.Logger?.Enabled == true; 66 | 67 | using (var channel = connection.CreateModel()) 68 | { 69 | foreach (var exchange in exchanges) 70 | { 71 | var name = exchange.Value; 72 | var type = options.Exchange.Type; 73 | if (loggerEnabled) 74 | { 75 | logger.LogInformation($"Declaring an exchange: '{name}', type: '{type}'."); 76 | } 77 | 78 | channel.ExchangeDeclare(name, type, options.Exchange.Durable, options.Exchange.AutoDelete); 79 | } 80 | } 81 | 82 | return connection; 83 | }); 84 | 85 | services.AddTransient(); 86 | services.AddTransient(); 87 | services.AddSingleton(); 88 | services.AddSingleton(); 89 | } 90 | 91 | public void Use(IApplicationBuilder app, IOptionsProvider optionsProvider) 92 | { 93 | app.UseRequestHandler(Name); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/RabbitMqOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada.Extensions.RabbitMq 4 | { 5 | public class RabbitMqOptions : IOptions 6 | { 7 | public string ConnectionName { get; set; } 8 | public IEnumerable Hostnames { get; set; } 9 | public int Port { get; set; } 10 | public string VirtualHost { get; set; } 11 | public string Username { get; set; } 12 | public string Password { get; set; } 13 | public int RequestedConnectionTimeout { get; set; } = 30000; 14 | public int SocketReadTimeout { get; set; } = 30000; 15 | public int SocketWriteTimeout { get; set; } = 30000; 16 | public ushort RequestedChannelMax { get; set; } 17 | public uint RequestedFrameMax { get; set; } 18 | public ushort RequestedHeartbeat { get; set; } 19 | public bool UseBackgroundThreadsForIO { get; set; } 20 | public ExchangeOptions Exchange { get; set; } 21 | public SslOptions Ssl { get; set; } 22 | public MessageContextOptions MessageContext { get; set; } 23 | public LoggerOptions Logger { get; set; } 24 | public string SpanContextHeader { get; set; } 25 | public IDictionary Headers { get; set; } 26 | 27 | public class SslOptions 28 | { 29 | public bool Enabled { get; set; } 30 | public string ServerName { get; set; } 31 | public string CertificatePath { get; set; } 32 | } 33 | 34 | public class ExchangeOptions 35 | { 36 | public bool DeclareExchange { get; set; } 37 | public bool Durable { get; set; } 38 | public bool AutoDelete { get; set; } 39 | public string Type { get; set; } 40 | } 41 | 42 | public class MessageContextOptions 43 | { 44 | public bool Enabled { get; set; } 45 | public string Header { get; set; } 46 | } 47 | 48 | public class LoggerOptions 49 | { 50 | public bool Enabled { get; set; } 51 | public string Level { get; set; } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.RabbitMq/rabbitmq.yml: -------------------------------------------------------------------------------- 1 | rabbitmq: 2 | connectionName: ntrada 3 | hostnames: 4 | - localhost 5 | port: 5672 6 | virtualHost: / 7 | username: guest 8 | password: guest 9 | requestedConnectionTimeout: 3000 10 | socketReadTimeout: 3000 11 | socketWriteTimeout: 3000 12 | requestedHeartbeat: 60 13 | exchange: 14 | declareExchange: true 15 | durable: true 16 | autoDelete: false 17 | type: topic 18 | messageContext: 19 | enabled: true 20 | header: message_context 21 | logger: 22 | enabled: true 23 | spanContextHeader: span_context -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Swagger/Ntrada.Extensions.Swagger.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Swagger/SwaggerExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.OpenApi.Models; 4 | 5 | namespace Ntrada.Extensions.Swagger 6 | { 7 | public class SwaggerExtension : IExtension 8 | { 9 | public string Name => "swagger"; 10 | public string Description => "Swagger Web API docs"; 11 | 12 | public void Add(IServiceCollection services, IOptionsProvider optionsProvider) 13 | { 14 | var options = optionsProvider.GetForExtension(Name); 15 | services.AddSingleton(options); 16 | services.AddSwaggerGen(c => 17 | { 18 | c.SwaggerDoc(options.Name, new OpenApiInfo {Title = options.Title, Version = options.Version}); 19 | c.DocumentFilter(); 20 | if (options.IncludeSecurity) 21 | { 22 | c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme 23 | { 24 | Description = 25 | "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", 26 | Name = "Authorization", 27 | In = ParameterLocation.Header, 28 | Type = SecuritySchemeType.ApiKey 29 | }); 30 | } 31 | }); 32 | } 33 | 34 | public void Use(IApplicationBuilder app, IOptionsProvider optionsProvider) 35 | { 36 | var options = optionsProvider.GetForExtension(Name); 37 | var routePrefix = string.IsNullOrWhiteSpace(options.RoutePrefix) ? "swagger" : options.RoutePrefix; 38 | app.UseStaticFiles() 39 | .UseSwagger(c => c.RouteTemplate = routePrefix + "/{documentName}/swagger.json"); 40 | 41 | if (options.ReDocEnabled) 42 | { 43 | app.UseReDoc(c => 44 | { 45 | c.RoutePrefix = routePrefix; 46 | c.SpecUrl = $"{options.Name}/swagger.json"; 47 | }); 48 | 49 | return; 50 | } 51 | 52 | app.UseSwaggerUI(c => 53 | { 54 | c.SwaggerEndpoint($"/{routePrefix}/{options.Name}/swagger.json", options.Title); 55 | c.RoutePrefix = routePrefix; 56 | }); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Swagger/SwaggerOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Extensions.Swagger 2 | { 3 | public class SwaggerOptions : IOptions 4 | { 5 | public bool ReDocEnabled { get; set; } 6 | public string Name { get; set; } 7 | public string Title { get; set; } 8 | public string Version { get; set; } 9 | public string RoutePrefix { get; set; } 10 | public bool IncludeSecurity { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Swagger/WebApiDocumentFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.OpenApi.Any; 5 | using Microsoft.OpenApi.Models; 6 | using Newtonsoft.Json; 7 | using Ntrada.WebApi; 8 | using Swashbuckle.AspNetCore.SwaggerGen; 9 | 10 | namespace Ntrada.Extensions.Swagger 11 | { 12 | internal sealed class WebApiDocumentFilter : IDocumentFilter 13 | { 14 | private readonly WebApiEndpointDefinitions _definitions; 15 | private const string InBody = "body"; 16 | private const string InQuery = "query"; 17 | 18 | private readonly Func _getOperation = 19 | (item, method) => 20 | { 21 | switch (method.ToLowerInvariant()) 22 | { 23 | case "get": 24 | item.AddOperation(OperationType.Get, new OpenApiOperation()); 25 | return (item.Operations[OperationType.Get], OperationType.Get); 26 | case "head": 27 | item.AddOperation(OperationType.Head, new OpenApiOperation()); 28 | return (item.Operations[OperationType.Head], OperationType.Head); 29 | case "options": 30 | item.AddOperation(OperationType.Options, new OpenApiOperation()); 31 | return (item.Operations[OperationType.Options], OperationType.Options); 32 | case "post": 33 | item.AddOperation(OperationType.Post, new OpenApiOperation()); 34 | return (item.Operations[OperationType.Post], OperationType.Post); 35 | case "put": 36 | item.AddOperation(OperationType.Put, new OpenApiOperation()); 37 | return (item.Operations[OperationType.Put], OperationType.Put); 38 | case "patch": 39 | item.AddOperation(OperationType.Patch, new OpenApiOperation()); 40 | return (item.Operations[OperationType.Patch], OperationType.Patch); 41 | case "delete": 42 | item.AddOperation(OperationType.Delete, new OpenApiOperation()); 43 | return (item.Operations[OperationType.Delete], OperationType.Delete); 44 | } 45 | 46 | return (default, default); 47 | }; 48 | 49 | public WebApiDocumentFilter(WebApiEndpointDefinitions definitions) 50 | => _definitions = definitions; 51 | 52 | public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) 53 | { 54 | foreach (var definition in _definitions) 55 | { 56 | var pathItem = new OpenApiPathItem(); 57 | var (operation, type) = _getOperation(pathItem, definition.Method); 58 | if (operation is null) 59 | { 60 | continue; 61 | } 62 | 63 | operation.Responses = new OpenApiResponses(); 64 | operation.Parameters = new List(); 65 | 66 | foreach (var parameter in definition.Parameters) 67 | { 68 | if (parameter.In is InBody) 69 | { 70 | operation.Parameters.Add(new OpenApiParameter 71 | { 72 | Name = parameter.Name, 73 | Schema = new OpenApiSchema 74 | { 75 | Type = parameter.Type, 76 | Example = new OpenApiString(JsonConvert.SerializeObject(parameter.Example)) 77 | } 78 | }); 79 | } 80 | else if (parameter.In is InQuery) 81 | { 82 | operation.Parameters.Add(new OpenApiParameter 83 | { 84 | Name = parameter.Name, 85 | Schema = new OpenApiSchema 86 | { 87 | Type = parameter.Type, 88 | Example = new OpenApiString(JsonConvert.SerializeObject(parameter.Example)) 89 | } 90 | }); 91 | } 92 | } 93 | 94 | foreach (var response in definition.Responses) 95 | { 96 | operation.Responses.Add(response.StatusCode.ToString(), new OpenApiResponse 97 | { 98 | Content = new Dictionary 99 | { 100 | { 101 | "body", new OpenApiMediaType 102 | { 103 | Schema = new OpenApiSchema 104 | { 105 | Type = response.Type, 106 | Example = new OpenApiString(JsonConvert.SerializeObject(response.Example)) 107 | } 108 | } 109 | } 110 | } 111 | }); 112 | } 113 | 114 | var path = definition.Path; 115 | if (string.IsNullOrWhiteSpace(path)) 116 | { 117 | path = "/"; 118 | } 119 | 120 | var (_, openApiPathItem) = swaggerDoc.Paths.SingleOrDefault(p => p.Key == path); 121 | if (openApiPathItem is {}) 122 | { 123 | openApiPathItem.AddOperation(type, operation); 124 | } 125 | else 126 | { 127 | swaggerDoc.Paths.Add($"{definition.Path}", pathItem); 128 | } 129 | } 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Swagger/rabbitmq.yml: -------------------------------------------------------------------------------- 1 | swagger: 2 | name: v1 3 | reDocEnabled: false 4 | title: API 5 | version: v1 6 | routePrefix: docs 7 | includeSecurity: true -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Tracing/DefaultTracer.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Jaeger; 3 | using Jaeger.Reporters; 4 | using Jaeger.Samplers; 5 | using OpenTracing; 6 | 7 | namespace Ntrada.Extensions.Tracing 8 | { 9 | public class DefaultTracer 10 | { 11 | public static ITracer Create() 12 | => new Tracer.Builder(Assembly.GetEntryAssembly().FullName) 13 | .WithReporter(new NoopReporter()) 14 | .WithSampler(new ConstSampler(false)) 15 | .Build(); 16 | } 17 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Tracing/JaegerHttpMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using OpenTracing; 5 | using OpenTracing.Tag; 6 | 7 | namespace Ntrada.Extensions.Tracing 8 | { 9 | internal sealed class JaegerHttpMiddleware 10 | { 11 | private readonly RequestDelegate _next; 12 | 13 | public JaegerHttpMiddleware(RequestDelegate next) 14 | { 15 | _next = next; 16 | } 17 | 18 | public async Task Invoke(HttpContext context, ITracer tracer) 19 | { 20 | IScope scope = null; 21 | var span = tracer.ActiveSpan; 22 | var method = context.Request.Method; 23 | if (span is null) 24 | { 25 | var spanBuilder = tracer.BuildSpan($"HTTP {method}"); 26 | scope = spanBuilder.StartActive(true); 27 | span = scope.Span; 28 | } 29 | 30 | span.Log($"Processing HTTP {method}: {context.Request.Path}"); 31 | try 32 | { 33 | await _next(context); 34 | } 35 | catch (Exception ex) 36 | { 37 | span.SetTag(Tags.Error, true); 38 | span.Log(ex.Message); 39 | throw; 40 | } 41 | finally 42 | { 43 | scope?.Dispose(); 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Tracing/Ntrada.Extensions.Tracing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Tracing/TracingExtension.cs: -------------------------------------------------------------------------------- 1 | using Jaeger; 2 | using Jaeger.Reporters; 3 | using Jaeger.Samplers; 4 | using Jaeger.Senders; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | using OpenTracing; 9 | using OpenTracing.Contrib.NetCore.Configuration; 10 | using OpenTracing.Util; 11 | 12 | namespace Ntrada.Extensions.Tracing 13 | { 14 | public class TracingExtension : IExtension 15 | { 16 | public string Name => "tracing"; 17 | public string Description => "Open Tracing using Jaeger"; 18 | 19 | public void Add(IServiceCollection services, IOptionsProvider optionsProvider) 20 | { 21 | var options = optionsProvider.GetForExtension("tracing"); 22 | services.AddOpenTracing(); 23 | services.AddSingleton(options); 24 | if (options.UseEmptyTracer) 25 | { 26 | var defaultTracer = DefaultTracer.Create(); 27 | services.AddSingleton(defaultTracer); 28 | return; 29 | } 30 | 31 | if (options.ExcludePaths is {}) 32 | { 33 | services.Configure(o => 34 | { 35 | foreach (var path in options.ExcludePaths) 36 | { 37 | o.Hosting.IgnorePatterns.Add(x => x.Request.Path == path); 38 | } 39 | }); 40 | } 41 | 42 | services.AddSingleton(sp => 43 | { 44 | var loggerFactory = sp.GetRequiredService(); 45 | 46 | var reporter = new RemoteReporter.Builder() 47 | .WithSender(new UdpSender(options.UdpHost, options.UdpPort, options.MaxPacketSize)) 48 | .WithLoggerFactory(loggerFactory) 49 | .Build(); 50 | 51 | var sampler = GetSampler(options); 52 | 53 | var tracer = new Tracer.Builder(options.ServiceName) 54 | .WithLoggerFactory(loggerFactory) 55 | .WithReporter(reporter) 56 | .WithSampler(sampler) 57 | .Build(); 58 | 59 | GlobalTracer.Register(tracer); 60 | 61 | return tracer; 62 | }); 63 | } 64 | 65 | public void Use(IApplicationBuilder app, IOptionsProvider optionsProvider) 66 | { 67 | app.UseMiddleware(); 68 | } 69 | 70 | private static ISampler GetSampler(TracingOptions options) 71 | { 72 | switch (options.Sampler) 73 | { 74 | case "const": return new ConstSampler(true); 75 | case "rate": return new RateLimitingSampler(options.MaxTracesPerSecond); 76 | case "probabilistic": return new ProbabilisticSampler(options.SamplingRate); 77 | default: return new ConstSampler(true); 78 | } 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Tracing/TracingOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada.Extensions.Tracing 4 | { 5 | public class TracingOptions : IOptions 6 | { 7 | public bool UseEmptyTracer { get; set; } 8 | public string ServiceName { get; set; } 9 | public string UdpHost { get; set; } 10 | public int UdpPort { get; set; } 11 | public int MaxPacketSize { get; set; } 12 | public string Sampler { get; set; } 13 | public double MaxTracesPerSecond { get; set; } = 5; 14 | public double SamplingRate { get; set; } = 0.2; 15 | public IEnumerable ExcludePaths { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /extensions/Ntrada.Extensions.Tracing/tracing.yml: -------------------------------------------------------------------------------- 1 | tracing: 2 | serviceName: ntrada 3 | udpHost: localhost 4 | udpPort: 6831 5 | maxPacketSize: 0 6 | sampler: const 7 | useEmptyTracer: false 8 | excludePaths: 9 | - /ping 10 | - /metrics -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Api/Ntrada.Samples.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Always 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Api/Ntrada.Samples.Api.rest: -------------------------------------------------------------------------------- 1 | @url = http://localhost:5000 2 | 3 | ### 4 | GET {{url}} 5 | 6 | ### 7 | POST {{url}} 8 | Content-Type: application/json 9 | 10 | { 11 | } 12 | 13 | ### 14 | GET {{url}}/orders 15 | 16 | ### 17 | POST {{url}}/orders 18 | # @name create_order 19 | Content-Type: application/json 20 | 21 | { 22 | "price": 300 23 | } 24 | 25 | 26 | 27 | @orderId = {{create_order.response.headers.Resource-ID}} 28 | 29 | ### 30 | GET {{url}}/orders/{{orderId}} 31 | 32 | ### 33 | DELETE {{url}}/orders/{{orderId}} 34 | -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | 9 | namespace Ntrada.Samples.Api 10 | { 11 | [ExcludeFromCodeCoverage] 12 | public class Program 13 | { 14 | public static Task Main(string[] args) 15 | => CreateHostBuilder(args).Build().RunAsync(); 16 | 17 | public static IHostBuilder CreateHostBuilder(string[] args) => 18 | Host.CreateDefaultBuilder(args) 19 | .ConfigureWebHostDefaults(webBuilder => 20 | { 21 | webBuilder.ConfigureAppConfiguration(builder => 22 | { 23 | const string extension = "yml"; 24 | var ntradaConfig = Environment.GetEnvironmentVariable("NTRADA_CONFIG"); 25 | var configPath = args?.FirstOrDefault() ?? ntradaConfig ?? $"ntrada.{extension}"; 26 | if (!configPath.EndsWith($".{extension}")) 27 | { 28 | configPath += $".{extension}"; 29 | } 30 | 31 | builder.AddYamlFile(configPath, false); 32 | }).UseStartup(); 33 | }); 34 | } 35 | } -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5000", 7 | "sslPort": 5001 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": false, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Ntrada.Samples.Api": { 19 | "commandName": "Project", 20 | "launchBrowser": false, 21 | "applicationUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Ntrada.Samples.Api 7 | { 8 | [ExcludeFromCodeCoverage] 9 | public class Startup 10 | { 11 | public void ConfigureServices(IServiceCollection services) 12 | { 13 | services.AddNtrada(); 14 | } 15 | 16 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 17 | { 18 | app.UseNtrada(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Api/ntrada.yml: -------------------------------------------------------------------------------- 1 | auth: 2 | enabled: true 3 | global: false 4 | claims: 5 | role: http://schemas.microsoft.com/ws/2008/06/identity/claims/role 6 | 7 | http: 8 | retries: 2 9 | interval: 2.0 10 | exponential: true 11 | 12 | useErrorHandler: true 13 | useJaeger: true 14 | useForwardedHeaders: true 15 | passQueryString: true 16 | modulesPath: Modules 17 | payloadsFolder: Payloads 18 | forwardRequestHeaders: true 19 | forwardResponseHeaders: true 20 | generateRequestId: true 21 | generateTraceId: true 22 | resourceId: 23 | generate: true 24 | property: id 25 | useLocalUrl: true 26 | loadBalancer: 27 | enabled: false 28 | url: localhost:9999 29 | 30 | extensions: 31 | customErrors: 32 | includeExceptionMessage: true 33 | 34 | cors: 35 | allowCredentials: true 36 | allowedOrigins: 37 | - '*' 38 | allowedMethods: 39 | - post 40 | - delete 41 | allowedHeaders: 42 | - '*' 43 | exposedHeaders: 44 | - Request-ID 45 | - Resource-ID 46 | - Trace-ID 47 | - Total-Count 48 | 49 | jwt: 50 | key: eiquief5phee9pazo0Faegaez9gohThailiur5woy2befiech1oarai4aiLi6ahVecah3ie9Aiz6Peij 51 | issuer: ntrada 52 | issuers: 53 | validateIssuer: true 54 | audience: 55 | audiences: 56 | validateAudience: false 57 | validateLifetime: true 58 | 59 | rabbitmq: 60 | enabled: true 61 | connectionName: ntrada 62 | hostnames: 63 | - localhost 64 | port: 5672 65 | virtualHost: / 66 | username: guest 67 | password: guest 68 | requestedConnectionTimeout: 3000 69 | socketReadTimeout: 3000 70 | socketWriteTimeout: 3000 71 | requestedHeartbeat: 60 72 | exchange: 73 | declareExchange: true 74 | durable: true 75 | autoDelete: false 76 | type: topic 77 | messageContext: 78 | enabled: true 79 | header: message_context 80 | logger: 81 | enabled: true 82 | spanContextHeader: span_context 83 | 84 | swagger: 85 | name: v1 86 | reDocEnabled: false 87 | title: API 88 | version: v1 89 | routePrefix: docs 90 | includeSecurity: true 91 | 92 | tracing: 93 | serviceName: ntrada 94 | udpHost: localhost 95 | udpPort: 6831 96 | maxPacketSize: 0 97 | sampler: const 98 | useEmptyTracer: false 99 | excludePaths: 100 | - /ping 101 | - /metrics 102 | 103 | modules: 104 | home: 105 | routes: 106 | - upstream: / 107 | method: GET 108 | use: return_value 109 | returnValue: Welcome to Ntrada API! 110 | 111 | - upstream: / 112 | method: POST 113 | auth: false 114 | use: rabbitmq 115 | config: 116 | exchange: sample.exchange 117 | routing_key: sample.routing.key 118 | 119 | orders: 120 | routes: 121 | - upstream: /orders 122 | # method: GET 123 | methods: 124 | - GET 125 | - POST 126 | - DELETE 127 | matchAll: true 128 | use: downstream 129 | downstream: orders-service/orders 130 | 131 | services: 132 | orders-service: 133 | localUrl: localhost:5001 134 | url: orders-service -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Services.Orders/Controllers/OrdersController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Ntrada.Samples.Services.Orders.Models; 6 | using Ntrada.Samples.Services.Orders.Requests; 7 | using Ntrada.Samples.Services.Orders.Services; 8 | 9 | namespace Ntrada.Samples.Services.Orders.Controllers 10 | { 11 | [ApiController] 12 | [Route("[controller]")] 13 | public class OrdersController : ControllerBase 14 | { 15 | private readonly IOrdersService _ordersService; 16 | 17 | public OrdersController(IOrdersService ordersService) 18 | { 19 | _ordersService = ordersService; 20 | } 21 | 22 | [HttpGet] 23 | public async Task>> Get() 24 | { 25 | var orders = await _ordersService.BrowseAsync(); 26 | 27 | return Ok(orders); 28 | } 29 | 30 | [HttpGet("{id}")] 31 | public async Task> Get(Guid id, string meta) 32 | { 33 | var order = await _ordersService.GetAsync(id); 34 | if (order is null) 35 | { 36 | return NotFound(); 37 | } 38 | 39 | order.Meta = meta; 40 | 41 | return order; 42 | } 43 | 44 | [HttpPost] 45 | public async Task Post(CreateOrder request) 46 | { 47 | try 48 | { 49 | await _ordersService.CreateAsync(request); 50 | return CreatedAtAction(nameof(Get), new {id = request.Id}, null); 51 | } 52 | catch (Exception ex) 53 | { 54 | return BadRequest(ex.Message); 55 | } 56 | } 57 | 58 | [HttpDelete("{id}")] 59 | public async Task Delete(Guid id) 60 | { 61 | await _ordersService.DeleteAsync(id); 62 | return NoContent(); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Services.Orders/Models/OrderDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ntrada.Samples.Services.Orders.Models 4 | { 5 | public class OrderDto 6 | { 7 | public Guid Id { get; set; } 8 | public decimal Price { get; set; } 9 | public string Meta { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Services.Orders/Ntrada.Samples.Services.Orders.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Services.Orders/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace Ntrada.Samples.Services.Orders 7 | { 8 | [ExcludeFromCodeCoverage] 9 | public class Program 10 | { 11 | public static Task Main(string[] args) 12 | => CreateHostBuilder(args).Build().RunAsync(); 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); 17 | } 18 | } -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Services.Orders/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5001", 7 | "sslPort": 5001 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": false, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Ntrada.Samples.Services.Orders": { 19 | "commandName": "Project", 20 | "launchBrowser": false, 21 | "applicationUrl": "http://localhost:5001", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Services.Orders/Requests/CreateOrder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ntrada.Samples.Services.Orders.Requests 4 | { 5 | public class CreateOrder 6 | { 7 | public Guid Id { get; } 8 | public decimal Price { get; } 9 | 10 | public CreateOrder(Guid id, decimal price) 11 | { 12 | Id = id == Guid.Empty ? Guid.NewGuid() : id; 13 | Price = price; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Services.Orders/Services/IOrdersService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Ntrada.Samples.Services.Orders.Models; 5 | using Ntrada.Samples.Services.Orders.Requests; 6 | 7 | namespace Ntrada.Samples.Services.Orders.Services 8 | { 9 | public interface IOrdersService 10 | { 11 | Task> BrowseAsync(); 12 | Task GetAsync(Guid id); 13 | Task CreateAsync(CreateOrder request); 14 | Task DeleteAsync(Guid id); 15 | } 16 | } -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Services.Orders/Services/OrdersService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Ntrada.Samples.Services.Orders.Models; 7 | using Ntrada.Samples.Services.Orders.Requests; 8 | 9 | namespace Ntrada.Samples.Services.Orders.Services 10 | { 11 | public class OrdersService : IOrdersService 12 | { 13 | private static readonly ConcurrentDictionary Orders = 14 | new ConcurrentDictionary 15 | { 16 | [GetGuid(1)] = new OrderDto 17 | { 18 | Id = GetGuid(1), 19 | Price = 100 20 | }, 21 | [GetGuid(2)] = new OrderDto 22 | { 23 | Id = GetGuid(2), 24 | Price = 200 25 | }, 26 | [GetGuid(3)] = new OrderDto 27 | { 28 | Id = GetGuid(3), 29 | Price = 300 30 | } 31 | }; 32 | 33 | public Task> BrowseAsync() 34 | { 35 | var orders = Orders.Values; 36 | 37 | return Task.FromResult(orders.AsEnumerable()); 38 | } 39 | 40 | public Task GetAsync(Guid id) 41 | { 42 | Orders.TryGetValue(id, out var order); 43 | 44 | return Task.FromResult(order); 45 | } 46 | 47 | public Task CreateAsync(CreateOrder request) 48 | { 49 | var added = Orders.TryAdd(request.Id, new OrderDto 50 | { 51 | Id = request.Id, 52 | Price = request.Price 53 | }); 54 | 55 | if (!added) 56 | { 57 | throw new ArgumentException($"Order with id: '{request.Id}' already exists."); 58 | } 59 | 60 | return Task.CompletedTask; 61 | } 62 | 63 | public Task DeleteAsync(Guid id) 64 | { 65 | Orders.TryRemove(id, out _); 66 | 67 | return Task.CompletedTask; 68 | } 69 | 70 | private static Guid GetGuid(int digit) => Guid.Parse($"{digit}0000000-0000-0000-0000-000000000000"); 71 | } 72 | } -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Services.Orders/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Ntrada.Samples.Services.Orders.Services; 7 | 8 | namespace Ntrada.Samples.Services.Orders 9 | { 10 | public class Startup 11 | { 12 | // This method gets called by the runtime. Use this method to add services to the container. 13 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 14 | public void ConfigureServices(IServiceCollection services) 15 | { 16 | services.AddMvcCore().AddNewtonsoftJson(); 17 | services.AddTransient(); 18 | } 19 | 20 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 21 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 22 | { 23 | if (env.IsDevelopment()) 24 | { 25 | app.UseDeveloperExceptionPage(); 26 | } 27 | 28 | app.UseRouting(); 29 | 30 | app.UseEndpoints(endpoints => 31 | { 32 | endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Orders Service"); }); 33 | endpoints.MapControllers(); 34 | }); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /samples/Ntrada.Samples.Services.Orders/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /src/Ntrada.Host/Ntrada.Host.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | Exe 6 | latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Always 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Ntrada.Host/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | namespace Ntrada.Host 9 | { 10 | [ExcludeFromCodeCoverage] 11 | public static class Program 12 | { 13 | public static Task Main(string[] args) 14 | => CreateHostBuilder(args).Build().RunAsync(); 15 | 16 | public static IHostBuilder CreateHostBuilder(string[] args) => 17 | Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args) 18 | .ConfigureWebHostDefaults(webBuilder => 19 | { 20 | webBuilder.ConfigureAppConfiguration(builder => 21 | { 22 | var configPath = args?.FirstOrDefault() ?? "ntrada.yml"; 23 | builder.AddYamlFile(configPath, false); 24 | }) 25 | .ConfigureServices(services => services.AddNtrada()) 26 | .Configure(app => app.UseNtrada()); 27 | }); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Ntrada.Host/ntrada.yml: -------------------------------------------------------------------------------- 1 | modules: 2 | home: 3 | routes: 4 | - upstream: / 5 | method: GET 6 | use: return_value 7 | returnValue: Welcome to Ntrada API! -------------------------------------------------------------------------------- /src/Ntrada/Auth/AuthenticationManager.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Http; 4 | using Ntrada.Options; 5 | 6 | namespace Ntrada.Auth 7 | { 8 | internal sealed class AuthenticationManager : IAuthenticationManager 9 | { 10 | private readonly NtradaOptions _options; 11 | 12 | public AuthenticationManager(NtradaOptions options) 13 | { 14 | _options = options; 15 | } 16 | 17 | 18 | public async Task TryAuthenticateAsync(HttpRequest request, RouteConfig routeConfig) 19 | { 20 | if (_options.Auth is null || !_options.Auth.Enabled || _options.Auth?.Global != true && 21 | routeConfig.Route?.Auth != true) 22 | { 23 | return true; 24 | } 25 | 26 | var result = await request.HttpContext.AuthenticateAsync(); 27 | 28 | return result.Succeeded; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Ntrada/Auth/AuthorizationManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Security.Claims; 4 | 5 | namespace Ntrada.Auth 6 | { 7 | internal sealed class AuthorizationManager : IAuthorizationManager 8 | { 9 | private readonly IPolicyManager _policyManager; 10 | 11 | public AuthorizationManager(IPolicyManager policyManager) 12 | { 13 | _policyManager = policyManager; 14 | } 15 | 16 | public bool IsAuthorized(ClaimsPrincipal user, RouteConfig routeConfig) 17 | { 18 | if (user is null) 19 | { 20 | return false; 21 | } 22 | 23 | if (routeConfig.Route is null) 24 | { 25 | return true; 26 | } 27 | 28 | return HasPolicies(user, routeConfig.Route.Policies) && HasClaims(user, routeConfig.Route.Claims); 29 | } 30 | 31 | private bool HasPolicies(ClaimsPrincipal user, IEnumerable policies) 32 | => policies is null || policies.All(p => HasPolicy(user, p)); 33 | 34 | private bool HasPolicy(ClaimsPrincipal user, string policy) 35 | => HasClaims(user, _policyManager.GetClaims(policy)); 36 | 37 | private static bool HasClaims(ClaimsPrincipal user, IDictionary claims) 38 | => claims is null || claims.All(claim => user.HasClaim(claim.Key, claim.Value)); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Ntrada/Auth/PolicyManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Ntrada.Configuration; 5 | using Ntrada.Options; 6 | 7 | namespace Ntrada.Auth 8 | { 9 | internal sealed class PolicyManager : IPolicyManager 10 | { 11 | private readonly IDictionary> _policies; 12 | private readonly NtradaOptions _options; 13 | 14 | public PolicyManager(NtradaOptions options) 15 | { 16 | _options = options; 17 | _policies = LoadPolicies(); 18 | } 19 | 20 | public IDictionary GetClaims(string policy) 21 | => _policies.TryGetValue(policy, out var claims) ? claims : null; 22 | 23 | private IDictionary> LoadPolicies() 24 | { 25 | var policies = (_options.Auth?.Policies ?? new Dictionary()) 26 | .ToDictionary(p => p.Key, p => p.Value.Claims.ToDictionary(c => c.Key, c => c.Value)); 27 | VerifyPolicies(policies); 28 | 29 | return policies; 30 | } 31 | 32 | private void VerifyPolicies(IDictionary> policies) 33 | { 34 | var definedPolicies = (_options.Modules ?? new Dictionary()) 35 | .Select(m => m.Value) 36 | .SelectMany(m => m.Routes ?? Enumerable.Empty()) 37 | .SelectMany(r => r.Policies ?? Enumerable.Empty()) 38 | .Distinct(); 39 | var missingPolicies = definedPolicies 40 | .Except(policies.Select(p => p.Key)) 41 | .ToArray(); 42 | if (missingPolicies.Any()) 43 | { 44 | throw new InvalidOperationException($"Missing policies: '{string.Join(", ", missingPolicies)}'"); 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Ntrada/Configuration/Auth.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada.Configuration 4 | { 5 | public class Auth 6 | { 7 | public bool Enabled { get; set; } 8 | public bool Global { get; set; } 9 | public IDictionary Policies { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Ntrada/Configuration/Http.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Configuration 2 | { 3 | public class Http 4 | { 5 | public int Retries { get; set; } 6 | public bool Exponential { get; set; } 7 | public double Interval { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ntrada/Configuration/LoadBalancer.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Configuration 2 | { 3 | public class LoadBalancer 4 | { 5 | public bool Enabled { get; set; } 6 | public string Url { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Ntrada/Configuration/Module.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada.Configuration 4 | { 5 | public class Module 6 | { 7 | public string Name { get; set; } 8 | public string Path { get; set; } 9 | public bool? Enabled { get; set; } 10 | public IEnumerable Routes { get; set; } 11 | public IDictionary Services { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Ntrada/Configuration/OnError.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Configuration 2 | { 3 | public class OnError 4 | { 5 | public int Code { get; set; } = 200; 6 | public object Data { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Ntrada/Configuration/OnSuccess.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Configuration 2 | { 3 | public class OnSuccess 4 | { 5 | public int Code { get; set; } = 200; 6 | public object Data { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Ntrada/Configuration/Policy.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada.Configuration 4 | { 5 | public class Policy 6 | { 7 | public IDictionary Claims { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ntrada/Configuration/ResourceId.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Configuration 2 | { 3 | public class ResourceId 4 | { 5 | public bool Generate { get; set; } 6 | public string Property { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Ntrada/Configuration/Route.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada.Configuration 4 | { 5 | public class Route 6 | { 7 | public ResourceId ResourceId { get; set; } 8 | public string Upstream { get; set; } 9 | public string Method { get; set; } 10 | public IEnumerable Methods { get; set; } 11 | public bool MatchAll { get; set; } 12 | public string Use { get; set; } 13 | public string Downstream { get; set; } 14 | public string DownstreamMethod { get; set; } 15 | public bool? PassQueryString { get; set; } 16 | public string ReturnValue { get; set; } 17 | public string Payload { get; set; } 18 | public string Schema { get; set; } 19 | public bool? Auth { get; set; } 20 | public IDictionary RequestHeaders { get; set; } 21 | public IDictionary ResponseHeaders { get; set; } 22 | public bool? ForwardRequestHeaders { get; set; } 23 | public bool? ForwardResponseHeaders { get; set; } 24 | public bool? ForwardStatusCode { get; set; } 25 | public bool? GenerateRequestId { get; set; } 26 | public bool? GenerateTraceId { get; set; } 27 | public OnError OnError { get; set; } 28 | public OnSuccess OnSuccess { get; set; } 29 | public IDictionary Claims { get; set; } 30 | public IEnumerable Policies { get; set; } 31 | public IEnumerable Bind { get; set; } 32 | public IEnumerable Transform { get; set; } 33 | public IDictionary Config { get; set; } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Ntrada/Configuration/Service.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Configuration 2 | { 3 | public class Service 4 | { 5 | public string LocalUrl { get; set; } 6 | public string Url { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Ntrada/Error.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada 2 | { 3 | public class Error 4 | { 5 | public string Code { get; set; } 6 | public string Property { get; set; } 7 | public string Message { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ntrada/ExecutionData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Dynamic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Routing; 6 | using Route = Ntrada.Configuration.Route; 7 | 8 | namespace Ntrada 9 | { 10 | public class ExecutionData 11 | { 12 | public string RequestId { get; set; } 13 | public string ResourceId { get; set; } 14 | public string TraceId { get; set; } 15 | public string UserId { get; set; } 16 | public IDictionary Claims { get; set; } 17 | public string ContentType { get; set; } 18 | public Route Route { get; set; } 19 | public HttpContext Context { get; set; } 20 | public RouteData Data { get; set; } 21 | public string Downstream { get; set; } 22 | public ExpandoObject Payload { get; set; } 23 | public bool HasPayload { get; set; } 24 | public IEnumerable ValidationErrors { get; set; } = Enumerable.Empty(); 25 | public bool IsPayloadValid => ValidationErrors is null || !ValidationErrors.Any(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Ntrada/Extensions/EnabledExtension.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Extensions 2 | { 3 | public sealed class EnabledExtension : IEnabledExtension 4 | { 5 | public IExtension Extension { get; } 6 | public IExtensionOptions Options { get; } 7 | 8 | public EnabledExtension(IExtension extension, IExtensionOptions options) 9 | { 10 | Extension = extension; 11 | Options = options; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Ntrada/Extensions/ExtensionOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.Extensions 2 | { 3 | public sealed class ExtensionOptions : IExtensionOptions 4 | { 5 | public int? Order { get; set; } 6 | public bool? Enabled { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Ntrada/Extensions/ExtensionProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Ntrada.Options; 5 | 6 | namespace Ntrada.Extensions 7 | { 8 | public sealed class ExtensionProvider : IExtensionProvider 9 | { 10 | private ISet _extensions = new HashSet(); 11 | 12 | private readonly NtradaOptions _options; 13 | 14 | public ExtensionProvider(NtradaOptions options) 15 | { 16 | _options = options; 17 | } 18 | 19 | public IEnumerable GetAll() 20 | { 21 | if (_extensions.Any()) 22 | { 23 | return _extensions; 24 | } 25 | 26 | var type = typeof(IExtension); 27 | var extensionTypes = AppDomain.CurrentDomain.GetAssemblies() 28 | .SelectMany(s => s.GetTypes()) 29 | .Where(p => type.IsAssignableFrom(p) && !p.IsInterface); 30 | var extensions = new HashSet(); 31 | foreach (var extensionType in extensionTypes) 32 | { 33 | var extension = (IExtension) Activator.CreateInstance(extensionType); 34 | var options = _options.Extensions?.SingleOrDefault(o => 35 | o.Key.Equals(extension.Name, StringComparison.InvariantCultureIgnoreCase)).Value; 36 | 37 | if (options is null) 38 | { 39 | continue; 40 | } 41 | 42 | extensions.Add(new EnabledExtension(extension, options)); 43 | } 44 | 45 | _extensions = new HashSet(extensions.OrderBy(e => e.Options.Order)); 46 | 47 | return _extensions; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Ntrada/Handlers/ReturnValueHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Ntrada.Hooks; 7 | using Route = Ntrada.Configuration.Route; 8 | 9 | namespace Ntrada.Handlers 10 | { 11 | internal sealed class ReturnValueHandler : IHandler 12 | { 13 | private readonly IRequestProcessor _requestProcessor; 14 | private readonly IEnumerable _requestHooks; 15 | private readonly IEnumerable _responseHooks; 16 | 17 | public ReturnValueHandler(IRequestProcessor requestProcessor, IServiceProvider serviceProvider) 18 | { 19 | _requestProcessor = requestProcessor; 20 | _requestHooks = serviceProvider.GetServices(); 21 | _responseHooks = serviceProvider.GetServices(); 22 | } 23 | 24 | public string GetInfo(Route route) => $"return a value: '{route.ReturnValue}'"; 25 | 26 | public async Task HandleAsync(HttpContext context, RouteConfig config) 27 | { 28 | var executionData = await _requestProcessor.ProcessAsync(config, context); 29 | if (_requestHooks is {}) 30 | { 31 | foreach (var hook in _requestHooks) 32 | { 33 | if (hook is null) 34 | { 35 | continue; 36 | } 37 | 38 | await hook.InvokeAsync(context.Request, executionData); 39 | } 40 | } 41 | 42 | if (_responseHooks is {}) 43 | { 44 | foreach (var hook in _responseHooks) 45 | { 46 | if (hook is null) 47 | { 48 | continue; 49 | } 50 | 51 | await hook.InvokeAsync(context.Response, executionData); 52 | } 53 | } 54 | 55 | await context.Response.WriteAsync(config.Route?.ReturnValue ?? string.Empty); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Ntrada/Helpers/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Runtime.Serialization; 6 | 7 | namespace Ntrada.Helpers 8 | { 9 | public static class Extensions 10 | { 11 | public static object SetDefaultInstanceProperties(this object instance) 12 | { 13 | var type = instance.GetType(); 14 | foreach (var propertyInfo in type.GetProperties()) 15 | { 16 | SetValue(propertyInfo, instance); 17 | } 18 | 19 | return instance; 20 | } 21 | 22 | private static void SetValue(PropertyInfo propertyInfo, object instance) 23 | { 24 | var propertyType = propertyInfo.PropertyType; 25 | if (propertyType == typeof(string)) 26 | { 27 | SetDefaultValue(propertyInfo, instance, string.Empty); 28 | return; 29 | } 30 | 31 | if (typeof(IEnumerable).IsAssignableFrom(propertyType)) 32 | { 33 | SetCollection(propertyInfo, instance); 34 | 35 | return; 36 | } 37 | 38 | if (propertyType.IsInterface) 39 | { 40 | return; 41 | } 42 | 43 | if (propertyType.IsArray) 44 | { 45 | SetCollection(propertyInfo, instance); 46 | return; 47 | } 48 | 49 | if (!propertyType.IsClass) 50 | { 51 | return; 52 | } 53 | 54 | var propertyInstance = FormatterServices.GetUninitializedObject(propertyInfo.PropertyType); 55 | SetDefaultValue(propertyInfo, instance, propertyInstance); 56 | SetDefaultInstanceProperties(propertyInstance); 57 | } 58 | 59 | private static void SetCollection(PropertyInfo propertyInfo, object instance) 60 | { 61 | var elementType = propertyInfo.PropertyType.IsGenericType 62 | ? propertyInfo.PropertyType.GenericTypeArguments[0] 63 | : propertyInfo.PropertyType.GetElementType(); 64 | if (elementType is null) 65 | { 66 | return; 67 | } 68 | 69 | if (typeof(IEnumerable).IsAssignableFrom(elementType)) 70 | { 71 | if (elementType == typeof(string)) 72 | { 73 | SetDefaultValue(propertyInfo, instance, Array.Empty()); 74 | return; 75 | } 76 | 77 | return; 78 | } 79 | 80 | var array = Array.CreateInstance(elementType, 0); 81 | SetDefaultValue(propertyInfo, instance, array); 82 | } 83 | 84 | private static void SetDefaultValue(PropertyInfo propertyInfo, object instance, object value) 85 | { 86 | if (propertyInfo.CanWrite) 87 | { 88 | propertyInfo.SetValue(instance, value); 89 | return; 90 | } 91 | 92 | var propertyName = propertyInfo.Name; 93 | var field = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic) 94 | .SingleOrDefault(x => x.Name.StartsWith($"<{propertyName}>")); 95 | field?.SetValue(instance, value); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/Ntrada/Hooks/IHttpRequestHook.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | 4 | namespace Ntrada.Hooks 5 | { 6 | public interface IHttpRequestHook 7 | { 8 | Task InvokeAsync(HttpRequestMessage request, ExecutionData data); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/Hooks/IHttpResponseHook.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | 4 | namespace Ntrada.Hooks 5 | { 6 | public interface IHttpResponseHook 7 | { 8 | Task InvokeAsync(HttpResponseMessage response, ExecutionData data); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/Hooks/IRequestHook.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Ntrada.Hooks 5 | { 6 | public interface IRequestHook 7 | { 8 | Task InvokeAsync(HttpRequest request, ExecutionData data); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/Hooks/IResponseHook.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Ntrada.Hooks 5 | { 6 | public interface IResponseHook 7 | { 8 | Task InvokeAsync(HttpResponse response, ExecutionData data); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/IAuthenticationManager.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Ntrada 5 | { 6 | public interface IAuthenticationManager 7 | { 8 | Task TryAuthenticateAsync(HttpRequest request, RouteConfig routeConfig); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/IAuthorizationManager.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace Ntrada 4 | { 5 | public interface IAuthorizationManager 6 | { 7 | bool IsAuthorized(ClaimsPrincipal user, RouteConfig routeConfig); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ntrada/IDownstreamBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Routing; 3 | 4 | namespace Ntrada 5 | { 6 | internal interface IDownstreamBuilder 7 | { 8 | string GetDownstream(RouteConfig routeConfig, HttpRequest request, RouteData data); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/IEnabledExtension.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada 2 | { 3 | public interface IEnabledExtension 4 | { 5 | IExtension Extension { get; } 6 | IExtensionOptions Options { get; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Ntrada/IExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Ntrada 5 | { 6 | public interface IExtension 7 | { 8 | string Name { get; } 9 | string Description { get; } 10 | void Add(IServiceCollection services, IOptionsProvider optionsProvider); 11 | void Use(IApplicationBuilder app, IOptionsProvider optionsProvider); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Ntrada/IExtensionOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada 2 | { 3 | public interface IExtensionOptions 4 | { 5 | int? Order { get; set; } 6 | bool? Enabled { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Ntrada/IExtensionProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada 4 | { 5 | internal interface IExtensionProvider 6 | { 7 | IEnumerable GetAll(); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ntrada/IHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Route = Ntrada.Configuration.Route; 4 | 5 | namespace Ntrada 6 | { 7 | public interface IHandler 8 | { 9 | string GetInfo(Route route); 10 | Task HandleAsync(HttpContext context, RouteConfig config); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Ntrada/IOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada 2 | { 3 | public interface IOptions 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /src/Ntrada/IOptionsProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada 2 | { 3 | public interface IOptionsProvider 4 | { 5 | T Get(string name = null) where T : class, IOptions, new(); 6 | T GetForExtension(string name) where T : class, IOptions, new(); 7 | } 8 | } -------------------------------------------------------------------------------- /src/Ntrada/IPayloadBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Ntrada 5 | { 6 | public interface IPayloadBuilder 7 | { 8 | Task BuildRawAsync(HttpRequest request); 9 | Task BuildJsonAsync(HttpRequest request) where T : class, new(); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Ntrada/IPayloadManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada 4 | { 5 | internal interface IPayloadManager 6 | { 7 | string GetKey(string method, string upstream); 8 | IDictionary Payloads { get; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/IPayloadTransformer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Routing; 3 | using Route = Ntrada.Configuration.Route; 4 | 5 | namespace Ntrada 6 | { 7 | internal interface IPayloadTransformer 8 | { 9 | bool HasTransformations(string resourceId, Route route); 10 | PayloadSchema Transform(string payload, string resourceId, Route route, HttpRequest request, RouteData data); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Ntrada/IPayloadValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Ntrada 6 | { 7 | public interface IPayloadValidator 8 | { 9 | Task TryValidate(ExecutionData executionData, HttpResponse httpResponse); 10 | Task> GetValidationErrorsAsync(PayloadSchema payloadSchema); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Ntrada/IPolicyManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada 4 | { 5 | internal interface IPolicyManager 6 | { 7 | IDictionary GetClaims(string policy); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ntrada/IRequestExecutionValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Ntrada 5 | { 6 | internal interface IRequestExecutionValidator 7 | { 8 | Task TryExecuteAsync(HttpContext context, RouteConfig routeConfig); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/IRequestHandlerManager.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Ntrada 5 | { 6 | public interface IRequestHandlerManager 7 | { 8 | IHandler Get(string name); 9 | void AddHandler(string name, IHandler handler); 10 | Task HandleAsync(string handler, HttpContext context, RouteConfig routeConfig); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Ntrada/IRequestProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Ntrada 5 | { 6 | public interface IRequestProcessor 7 | { 8 | Task ProcessAsync(RouteConfig routeConfig, HttpContext context); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/IRouteConfigurator.cs: -------------------------------------------------------------------------------- 1 | using Ntrada.Configuration; 2 | 3 | namespace Ntrada 4 | { 5 | internal interface IRouteConfigurator 6 | { 7 | RouteConfig Configure(Module module, Route route); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ntrada/IRouteProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Routing; 3 | 4 | namespace Ntrada 5 | { 6 | internal interface IRouteProvider 7 | { 8 | Action Build(); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/ISchemaValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | namespace Ntrada 5 | { 6 | internal interface ISchemaValidator 7 | { 8 | Task> ValidateAsync(string payload, string schema); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/IUpstreamBuilder.cs: -------------------------------------------------------------------------------- 1 | using Ntrada.Configuration; 2 | 3 | namespace Ntrada 4 | { 5 | internal interface IUpstreamBuilder 6 | { 7 | string Build(Module module, Route route); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Ntrada/IValueProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Routing; 4 | 5 | namespace Ntrada 6 | { 7 | internal interface IValueProvider 8 | { 9 | IEnumerable Tokens { get; } 10 | string Get(string value, HttpRequest request, RouteData data); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Ntrada/Ntrada.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Ntrada API Gateway. 5 | snatch.dev 6 | Ntrada 7 | Ntrada 8 | Ntrada 9 | https://github.com/snatch-dev/Ntrada 10 | https://github.com/snatch-dev/Ntrada/blob/master/LICENSE 11 | 12 | 13 | 14 | netcoreapp3.1 15 | latest 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Ntrada/NtradaExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.CompilerServices; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.HttpOverrides; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json; 11 | using Ntrada.Auth; 12 | using Ntrada.Configuration; 13 | using Ntrada.Extensions; 14 | using Ntrada.Handlers; 15 | using Ntrada.Options; 16 | using Ntrada.Requests; 17 | using Ntrada.Routing; 18 | using Ntrada.WebApi; 19 | using Polly; 20 | 21 | [assembly: InternalsVisibleTo("Ntrada.Tests.Unit")] 22 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 23 | namespace Ntrada 24 | { 25 | public static class NtradaExtensions 26 | { 27 | private const string Logo = @" 28 | / | / / /__________ _____/ /___ _ 29 | / |/ / __/ ___/ __ `/ __ / __ `/ 30 | / /| / /_/ / / /_/ / /_/ / /_/ / 31 | /_/ |_/\__/_/ \__,_/\__,_/\__,_/ 32 | 33 | 34 | /___ API Gateway (Entrance) ___/ 35 | 36 | 37 | https://github.com/snatch-dev/Ntrada 38 | "; 39 | 40 | 41 | public static IServiceCollection AddNtrada(this IServiceCollection services) 42 | { 43 | var (configuration, optionsProvider) = BuildConfiguration(services); 44 | 45 | return services.AddCoreServices() 46 | .ConfigureLogging(configuration) 47 | .ConfigureHttpClient(configuration) 48 | .ConfigurePayloads(configuration) 49 | .AddNtradaServices() 50 | .AddExtensions(optionsProvider); 51 | } 52 | 53 | private static (NtradaOptions, OptionsProvider) BuildConfiguration(IServiceCollection services) 54 | { 55 | IConfiguration config; 56 | using (var scope = services.BuildServiceProvider().CreateScope()) 57 | { 58 | config = scope.ServiceProvider.GetService(); 59 | } 60 | 61 | var optionsProvider = new OptionsProvider(config); 62 | services.AddSingleton(optionsProvider); 63 | var options = optionsProvider.Get(); 64 | services.AddSingleton(options); 65 | 66 | return (options, optionsProvider); 67 | } 68 | 69 | private static IServiceCollection AddCoreServices(this IServiceCollection services) 70 | { 71 | services.AddMvcCore() 72 | .AddNewtonsoftJson(o => o.SerializerSettings.Formatting = Formatting.Indented) 73 | .AddApiExplorer(); 74 | 75 | return services; 76 | } 77 | 78 | private static IServiceCollection ConfigureLogging(this IServiceCollection services, NtradaOptions options) 79 | { 80 | services.AddLogging( 81 | builder => 82 | { 83 | builder.AddFilter("Microsoft", LogLevel.Warning) 84 | .AddFilter("System", LogLevel.Warning) 85 | .AddConsole(); 86 | }); 87 | 88 | return services; 89 | } 90 | 91 | private static IServiceCollection ConfigureHttpClient(this IServiceCollection services, NtradaOptions options) 92 | { 93 | var http = options.Http ?? new Http(); 94 | var httpClientBuilder = services.AddHttpClient("ntrada"); 95 | httpClientBuilder.AddTransientHttpErrorPolicy(p => 96 | p.WaitAndRetryAsync(http.Retries, retryAttempt => 97 | { 98 | var interval = http.Exponential 99 | ? Math.Pow(http.Interval, retryAttempt) 100 | : http.Interval; 101 | 102 | return TimeSpan.FromSeconds(interval); 103 | })); 104 | 105 | return services; 106 | } 107 | 108 | private static IServiceCollection ConfigurePayloads(this IServiceCollection services, NtradaOptions options) 109 | { 110 | if (options.PayloadsFolder is null) 111 | { 112 | options.PayloadsFolder = "Payloads"; 113 | } 114 | 115 | if (options.PayloadsFolder.EndsWith("/")) 116 | { 117 | options.PayloadsFolder = options.PayloadsFolder 118 | .Substring(0, options.PayloadsFolder.Length - 1); 119 | } 120 | 121 | return services; 122 | } 123 | 124 | private static IServiceCollection AddNtradaServices(this IServiceCollection services) 125 | { 126 | services.AddSingleton(); 127 | services.AddSingleton(); 128 | services.AddSingleton(); 129 | services.AddSingleton(); 130 | services.AddSingleton(); 131 | services.AddSingleton(); 132 | services.AddSingleton(); 133 | services.AddSingleton(); 134 | services.AddSingleton(); 135 | services.AddSingleton(); 136 | services.AddSingleton(); 137 | services.AddSingleton(); 138 | services.AddSingleton(); 139 | services.AddSingleton(); 140 | services.AddSingleton(); 141 | services.AddSingleton(); 142 | services.AddSingleton(); 143 | services.AddSingleton(); 144 | services.AddSingleton(); 145 | 146 | return services; 147 | } 148 | 149 | private static IServiceCollection AddExtensions(this IServiceCollection services, IOptionsProvider optionsProvider) 150 | { 151 | var options = optionsProvider.Get(); 152 | var extensionProvider = new ExtensionProvider(options); 153 | services.AddSingleton(extensionProvider); 154 | 155 | foreach (var extension in extensionProvider.GetAll()) 156 | { 157 | if (extension.Options.Enabled == false) 158 | { 159 | continue; 160 | } 161 | 162 | extension.Extension.Add(services, optionsProvider); 163 | } 164 | 165 | return services; 166 | } 167 | 168 | public static IApplicationBuilder UseNtrada(this IApplicationBuilder app) 169 | { 170 | var newLine = Environment.NewLine; 171 | var logger = app.ApplicationServices.GetRequiredService>(); 172 | logger.LogInformation($"{newLine}{newLine}{Logo}{newLine}{newLine}"); 173 | var options = app.ApplicationServices.GetRequiredService(); 174 | if (options.Auth?.Enabled == true) 175 | { 176 | logger.LogInformation("Authentication is enabled."); 177 | app.UseAuthentication(); 178 | } 179 | else 180 | { 181 | logger.LogInformation("Authentication is disabled."); 182 | } 183 | 184 | if (options.UseForwardedHeaders) 185 | { 186 | logger.LogInformation("Headers forwarding is enabled."); 187 | app.UseForwardedHeaders(new ForwardedHeadersOptions 188 | { 189 | ForwardedHeaders = ForwardedHeaders.All 190 | }); 191 | } 192 | 193 | if (options.LoadBalancer?.Enabled == true) 194 | { 195 | logger.LogInformation($"Load balancer is enabled: {options.LoadBalancer.Url}"); 196 | } 197 | 198 | app.UseExtensions(); 199 | app.RegisterRequestHandlers(); 200 | app.AddRoutes(); 201 | 202 | return app; 203 | } 204 | 205 | private static void RegisterRequestHandlers(this IApplicationBuilder app) 206 | { 207 | var logger = app.ApplicationServices.GetRequiredService>(); 208 | var options = app.ApplicationServices.GetRequiredService(); 209 | var requestHandlerManager = app.ApplicationServices.GetRequiredService(); 210 | requestHandlerManager.AddHandler("downstream", 211 | app.ApplicationServices.GetRequiredService()); 212 | requestHandlerManager.AddHandler("return_value", 213 | app.ApplicationServices.GetRequiredService()); 214 | 215 | if (options.Modules is null) 216 | { 217 | return; 218 | } 219 | 220 | var handlers = options.Modules 221 | .Select(m => m.Value) 222 | .SelectMany(m => m.Routes) 223 | .Select(r => r.Use) 224 | .Distinct() 225 | .ToArray(); 226 | 227 | foreach (var handler in handlers) 228 | { 229 | if (requestHandlerManager.Get(handler) is null) 230 | { 231 | throw new Exception($"Handler: '{handler}' was not defined."); 232 | } 233 | 234 | logger.LogInformation($"Added handler: '{handler}'"); 235 | } 236 | } 237 | 238 | private static void AddRoutes(this IApplicationBuilder app) 239 | { 240 | var options = app.ApplicationServices.GetRequiredService(); 241 | if (options.Modules is null) 242 | { 243 | return; 244 | } 245 | 246 | foreach (var route in options.Modules.SelectMany(m => m.Value.Routes)) 247 | { 248 | if (route.Methods is {}) 249 | { 250 | if (route.Methods.Any(m => m.Equals(route.Method, StringComparison.InvariantCultureIgnoreCase))) 251 | { 252 | throw new ArgumentException($"There's already a method {route.Method.ToUpperInvariant()} declared in route 'methods', as well as in 'method'."); 253 | } 254 | 255 | continue; 256 | } 257 | 258 | route.Method = (string.IsNullOrWhiteSpace(route.Method) ? "get" : route.Method).ToLowerInvariant(); 259 | route.DownstreamMethod = 260 | (string.IsNullOrWhiteSpace(route.DownstreamMethod) ? route.Method : route.DownstreamMethod) 261 | .ToLowerInvariant(); 262 | } 263 | 264 | var routeProvider = app.ApplicationServices.GetRequiredService(); 265 | app.UseRouting(); 266 | app.UseEndpoints(routeProvider.Build()); 267 | } 268 | 269 | private static void UseExtensions(this IApplicationBuilder app) 270 | { 271 | var logger = app.ApplicationServices.GetRequiredService>(); 272 | var optionsProvider = app.ApplicationServices.GetRequiredService(); 273 | var extensionProvider = app.ApplicationServices.GetRequiredService(); 274 | foreach (var extension in extensionProvider.GetAll()) 275 | { 276 | if (extension.Options.Enabled == false) 277 | { 278 | continue; 279 | } 280 | 281 | extension.Extension.Use(app, optionsProvider); 282 | var orderMessage = extension.Options.Order.HasValue 283 | ? $" [order: {extension.Options.Order}]" 284 | : string.Empty; 285 | logger.LogInformation($"Enabled extension: '{extension.Extension.Name}' " + 286 | $"({extension.Extension.Description}){orderMessage}"); 287 | } 288 | } 289 | 290 | private class Ntrada 291 | { 292 | } 293 | 294 | public static IApplicationBuilder UseRequestHandler(this IApplicationBuilder app, string name) 295 | where T : IHandler 296 | { 297 | var requestHandlerManager = app.ApplicationServices.GetRequiredService(); 298 | var handler = app.ApplicationServices.GetRequiredService(); 299 | requestHandlerManager.AddHandler(name, handler); 300 | 301 | return app; 302 | } 303 | 304 | public static bool IsNotEmpty(this IEnumerable enumerable) => !enumerable.IsEmpty(); 305 | 306 | public static bool IsEmpty(this IEnumerable enumerable) => enumerable is null || !enumerable.Any(); 307 | } 308 | } -------------------------------------------------------------------------------- /src/Ntrada/Options/NtradaOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Ntrada.Configuration; 3 | using Ntrada.Extensions; 4 | 5 | namespace Ntrada.Options 6 | { 7 | public class NtradaOptions : IOptions 8 | { 9 | public bool UseForwardedHeaders { get; set; } 10 | public bool? ForwardRequestHeaders { get; set; } 11 | public bool? ForwardResponseHeaders { get; set; } 12 | public IDictionary RequestHeaders { get; set; } 13 | public IDictionary ResponseHeaders { get; set; } 14 | public bool? ForwardStatusCode { get; set; } 15 | public bool? PassQueryString { get; set; } 16 | public Configuration.Auth Auth { get; set; } 17 | public string ModulesPath { get; set; } 18 | public string PayloadsFolder { get; set; } 19 | public ResourceId ResourceId { get; set; } 20 | public bool? GenerateRequestId { get; set; } 21 | public bool? GenerateTraceId { get; set; } 22 | public IDictionary Modules { get; set; } 23 | public Http Http { get; set; } 24 | public LoadBalancer LoadBalancer { get; set; } 25 | public bool UseLocalUrl { get; set; } 26 | public IDictionary Extensions { get; set; } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Ntrada/Options/OptionsProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace Ntrada.Options 4 | { 5 | internal class OptionsProvider : IOptionsProvider 6 | { 7 | private readonly IConfiguration _configuration; 8 | 9 | public OptionsProvider(IConfiguration configuration) 10 | { 11 | _configuration = configuration; 12 | } 13 | 14 | public T Get(string name = null) where T : class, IOptions, new() 15 | => GetOptions(name); 16 | 17 | public T GetForExtension(string name) where T : class, IOptions, new() 18 | => GetOptions(name, "extensions"); 19 | 20 | private T GetOptions(string name, string section = null) where T : class, IOptions, new() 21 | { 22 | var sectionName = string.IsNullOrWhiteSpace(section) 23 | ? string.IsNullOrWhiteSpace(name) ? string.Empty : name 24 | : $"{section}:{name}"; 25 | var options = new T(); 26 | if (string.IsNullOrWhiteSpace(sectionName)) 27 | { 28 | _configuration.Bind(options); 29 | } 30 | else 31 | { 32 | _configuration.GetSection(sectionName).Bind(options); 33 | } 34 | 35 | return options; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Ntrada/PayloadSchema.cs: -------------------------------------------------------------------------------- 1 | using System.Dynamic; 2 | 3 | namespace Ntrada 4 | { 5 | public class PayloadSchema 6 | { 7 | public ExpandoObject Payload { get; } 8 | public string Schema { get; } 9 | 10 | public PayloadSchema(ExpandoObject payload, string schema) 11 | { 12 | Payload = payload; 13 | Schema = schema; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Ntrada/Requests/PayloadBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Newtonsoft.Json; 5 | 6 | namespace Ntrada.Requests 7 | { 8 | internal sealed class PayloadBuilder : IPayloadBuilder 9 | { 10 | public async Task BuildRawAsync(HttpRequest request) 11 | { 12 | var content = string.Empty; 13 | if (request.Body == null) 14 | { 15 | return content; 16 | } 17 | 18 | using (var reader = new StreamReader(request.Body)) 19 | { 20 | content = await reader.ReadToEndAsync(); 21 | } 22 | 23 | return content; 24 | } 25 | 26 | public async Task BuildJsonAsync(HttpRequest request) where T : class, new() 27 | { 28 | var payload = await BuildRawAsync(request); 29 | 30 | return string.IsNullOrWhiteSpace(payload) ? new T() : JsonConvert.DeserializeObject(payload); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Ntrada/Requests/PayloadManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Dynamic; 3 | using System.IO; 4 | using Newtonsoft.Json; 5 | using Ntrada.Options; 6 | 7 | namespace Ntrada.Requests 8 | { 9 | internal sealed class PayloadManager : IPayloadManager 10 | { 11 | private readonly NtradaOptions _options; 12 | public IDictionary Payloads { get; } 13 | 14 | public PayloadManager(NtradaOptions options) 15 | { 16 | _options = options; 17 | Payloads = LoadPayloads(); 18 | } 19 | 20 | private IDictionary LoadPayloads() 21 | { 22 | if (_options.Modules is null) 23 | { 24 | return new Dictionary(); 25 | } 26 | 27 | var payloads = new Dictionary(); 28 | var modulesPath = _options.ModulesPath; 29 | modulesPath = string.IsNullOrWhiteSpace(modulesPath) 30 | ? string.Empty 31 | : (modulesPath.EndsWith("/") ? modulesPath : $"{modulesPath}/"); 32 | 33 | foreach (var module in _options.Modules) 34 | { 35 | foreach (var route in module.Value.Routes) 36 | { 37 | if (string.IsNullOrWhiteSpace(route.Payload)) 38 | { 39 | continue; 40 | } 41 | 42 | var payloadsFolder = _options.PayloadsFolder; 43 | var fullPath = $"{modulesPath}{module.Value.Name}/{payloadsFolder}/{route.Payload}"; 44 | var fullJsonPath = fullPath.EndsWith(".json") ? fullPath : $"{fullPath}.json"; 45 | if (!File.Exists(fullJsonPath)) 46 | { 47 | continue; 48 | } 49 | 50 | var schemaPath = $"{modulesPath}{module.Value.Name}/{payloadsFolder}/{route.Schema}"; 51 | var fullSchemaPath = schemaPath.EndsWith(".json") ? schemaPath : $"{schemaPath}.json"; 52 | var schema = string.Empty; 53 | if (File.Exists(fullSchemaPath)) 54 | { 55 | schema = File.ReadAllText(fullSchemaPath); 56 | } 57 | 58 | var json = File.ReadAllText(fullJsonPath); 59 | dynamic expandoObject = new ExpandoObject(); 60 | JsonConvert.PopulateObject(json, expandoObject); 61 | var upstream = string.IsNullOrWhiteSpace(route.Upstream) ? string.Empty : route.Upstream; 62 | if (!string.IsNullOrWhiteSpace(module.Value.Path)) 63 | { 64 | var modulePath = module.Value.Path.EndsWith("/") ? module.Value.Path : $"{module.Value.Path}/"; 65 | if (upstream.StartsWith("/")) 66 | { 67 | upstream = upstream.Substring(1, upstream.Length - 1); 68 | } 69 | 70 | if (upstream.EndsWith("/")) 71 | { 72 | upstream = upstream.Substring(0, upstream.Length - 1); 73 | } 74 | 75 | upstream = $"{modulePath}{upstream}"; 76 | } 77 | 78 | if (string.IsNullOrWhiteSpace(upstream)) 79 | { 80 | upstream = "/"; 81 | } 82 | 83 | payloads.Add(GetKey(route.Method, upstream), new PayloadSchema(expandoObject, schema)); 84 | } 85 | } 86 | 87 | return payloads; 88 | } 89 | 90 | public string GetKey(string method, string upstream) => $"{method?.ToLowerInvariant()}:{upstream}"; 91 | } 92 | } -------------------------------------------------------------------------------- /src/Ntrada/Requests/PayloadTransformer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Dynamic; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Routing; 6 | using Newtonsoft.Json; 7 | using Ntrada.Options; 8 | using Route = Ntrada.Configuration.Route; 9 | 10 | namespace Ntrada.Requests 11 | { 12 | internal sealed class PayloadTransformer : IPayloadTransformer 13 | { 14 | private const string ResourceIdProperty = "id"; 15 | private readonly NtradaOptions _options; 16 | private readonly IPayloadManager _payloadManager; 17 | private readonly IValueProvider _valueProvider; 18 | private readonly IDictionary _payloads; 19 | 20 | public PayloadTransformer(NtradaOptions options, IPayloadManager payloadManager, IValueProvider valueProvider) 21 | { 22 | _options = options; 23 | _payloadManager = payloadManager; 24 | _valueProvider = valueProvider; 25 | _payloads = payloadManager.Payloads; 26 | } 27 | 28 | public bool HasTransformations(string resourceId, Route route) 29 | { 30 | if (!string.IsNullOrWhiteSpace(resourceId)) 31 | { 32 | return true; 33 | } 34 | 35 | if (route.Bind.IsNotEmpty()) 36 | { 37 | return true; 38 | } 39 | 40 | return route.Transform.IsNotEmpty() || _payloads.ContainsKey(GetPayloadKey(route)); 41 | } 42 | 43 | public PayloadSchema Transform(string payload, string resourceId, Route route, HttpRequest request, RouteData data) 44 | { 45 | var payloadKey = GetPayloadKey(route); 46 | var command = _payloads.ContainsKey(payloadKey) 47 | ? GetObjectFromPayload(route, payload) 48 | : GetObject(payload); 49 | 50 | var commandValues = (IDictionary) command; 51 | if (!string.IsNullOrWhiteSpace(resourceId)) 52 | { 53 | var resourceIdProperty = string.IsNullOrWhiteSpace(route.ResourceId?.Property) 54 | ? _options.ResourceId?.Property 55 | : route.ResourceId.Property; 56 | if (string.IsNullOrWhiteSpace(resourceIdProperty)) 57 | { 58 | resourceIdProperty = ResourceIdProperty; 59 | } 60 | 61 | commandValues[resourceIdProperty] = resourceId; 62 | } 63 | 64 | foreach (var setter in route.Bind ?? Enumerable.Empty()) 65 | { 66 | var keyAndValue = setter.Split(':'); 67 | var key = keyAndValue[0]; 68 | var value = keyAndValue[1]; 69 | commandValues[key] = _valueProvider.Get(value, request, data); 70 | var routeValue = value.Length > 2 ? value.Substring(1, value.Length - 2) : string.Empty; 71 | if (data.Values.TryGetValue(routeValue, out var dataValue)) 72 | { 73 | commandValues[key] = dataValue; 74 | } 75 | } 76 | 77 | foreach (var transformation in route.Transform ?? Enumerable.Empty()) 78 | { 79 | var beforeAndAfter = transformation.Split(':'); 80 | var before = beforeAndAfter[0]; 81 | var after = beforeAndAfter[1]; 82 | if (!commandValues.TryGetValue(before, out var value)) 83 | { 84 | continue; 85 | } 86 | 87 | commandValues.Remove(before); 88 | commandValues.Add(after, value); 89 | } 90 | 91 | _payloads.TryGetValue(payloadKey, out var payloadSchema); 92 | 93 | return new PayloadSchema(command as ExpandoObject, payloadSchema?.Schema); 94 | } 95 | 96 | private object GetObjectFromPayload(Route route, string content) 97 | { 98 | var payloadValue = _payloads[GetPayloadKey(route)].Payload; 99 | var request = JsonConvert.DeserializeObject(content, payloadValue.GetType()); 100 | var payloadValues = (IDictionary) payloadValue; 101 | var requestValues = (IDictionary) request; 102 | 103 | foreach (var key in requestValues.Keys) 104 | { 105 | if (!payloadValues.ContainsKey(key)) 106 | { 107 | requestValues.Remove(key); 108 | } 109 | } 110 | 111 | return request; 112 | } 113 | 114 | private static object GetObject(string content) 115 | { 116 | dynamic payload = new ExpandoObject(); 117 | JsonConvert.PopulateObject(content, payload); 118 | 119 | return JsonConvert.DeserializeObject(content, payload.GetType()); 120 | } 121 | 122 | private string GetPayloadKey(Route route) => _payloadManager.GetKey(route.Method, route.Upstream); 123 | } 124 | } -------------------------------------------------------------------------------- /src/Ntrada/Requests/PayloadValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Newtonsoft.Json; 6 | 7 | namespace Ntrada.Requests 8 | { 9 | internal sealed class PayloadValidator : IPayloadValidator 10 | { 11 | private readonly ISchemaValidator _schemaValidator; 12 | 13 | public PayloadValidator(ISchemaValidator schemaValidator) 14 | { 15 | _schemaValidator = schemaValidator; 16 | } 17 | 18 | public async Task TryValidate(ExecutionData executionData, HttpResponse httpResponse) 19 | { 20 | if (executionData.IsPayloadValid) 21 | { 22 | return true; 23 | } 24 | 25 | var response = new {errors = executionData.ValidationErrors}; 26 | var payload = JsonConvert.SerializeObject(response); 27 | httpResponse.ContentType = "application/json"; 28 | await httpResponse.WriteAsync(payload); 29 | 30 | return false; 31 | } 32 | 33 | public async Task> GetValidationErrorsAsync(PayloadSchema payloadSchema) 34 | { 35 | if (string.IsNullOrWhiteSpace(payloadSchema.Schema)) 36 | { 37 | return Enumerable.Empty(); 38 | } 39 | 40 | return await _schemaValidator.ValidateAsync(JsonConvert.SerializeObject(payloadSchema.Payload), 41 | payloadSchema.Schema); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Ntrada/Requests/RequestHandlerManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Ntrada.Requests 8 | { 9 | internal sealed class RequestHandlerManager : IRequestHandlerManager 10 | { 11 | private readonly ILogger _logger; 12 | 13 | private static readonly ConcurrentDictionary Handlers = 14 | new ConcurrentDictionary(); 15 | 16 | public RequestHandlerManager(ILogger logger) 17 | { 18 | _logger = logger; 19 | } 20 | 21 | public IHandler Get(string name) => Handlers.TryGetValue(name, out var handler) ? handler : null; 22 | 23 | public void AddHandler(string name, IHandler handler) 24 | { 25 | if (Handlers.TryAdd(name, handler)) 26 | { 27 | _logger.LogInformation($"Added a request handler: '{name}'"); 28 | return; 29 | } 30 | 31 | _logger.LogError($"Couldn't add a request handler: '{name}'"); 32 | } 33 | 34 | public async Task HandleAsync(string handler, HttpContext context, RouteConfig routeConfig) 35 | { 36 | if (!Handlers.TryGetValue(handler, out var instance)) 37 | { 38 | throw new Exception($"Handler: '{handler}' was not found."); 39 | } 40 | 41 | await instance.HandleAsync(context, routeConfig); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Ntrada/Requests/RequestProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Routing; 7 | using Ntrada.Options; 8 | 9 | namespace Ntrada.Requests 10 | { 11 | internal sealed class RequestProcessor : IRequestProcessor 12 | { 13 | private static readonly string[] SkipPayloadMethods = {"get", "delete", "head", "options", "trace"}; 14 | private static readonly IDictionary EmptyClaims = new Dictionary(); 15 | private const string ContentTypeApplicationJson = "application/json"; 16 | private const string ContentTypeTextPlain = "text/plain"; 17 | private const string ContentTypeHeader = "Content-Type"; 18 | private readonly NtradaOptions _options; 19 | private readonly IPayloadTransformer _payloadTransformer; 20 | private readonly IPayloadBuilder _payloadBuilder; 21 | private readonly IPayloadValidator _payloadValidator; 22 | private readonly IDownstreamBuilder _downstreamBuilder; 23 | 24 | public RequestProcessor(NtradaOptions options, IPayloadTransformer payloadTransformer, 25 | IPayloadBuilder payloadBuilder, IPayloadValidator payloadValidator, IDownstreamBuilder downstreamBuilder) 26 | { 27 | _options = options; 28 | _payloadTransformer = payloadTransformer; 29 | _payloadBuilder = payloadBuilder; 30 | _payloadValidator = payloadValidator; 31 | _downstreamBuilder = downstreamBuilder; 32 | } 33 | 34 | public async Task ProcessAsync(RouteConfig routeConfig, HttpContext context) 35 | { 36 | context.Request.Headers.TryGetValue(ContentTypeHeader, out var contentType); 37 | var contentTypeValue = contentType.ToString().ToLowerInvariant(); 38 | if (string.IsNullOrWhiteSpace(contentTypeValue) || contentTypeValue.Contains(ContentTypeTextPlain)) 39 | { 40 | contentTypeValue = ContentTypeApplicationJson; 41 | } 42 | 43 | var (requestId, resourceId, traceId) = GenerateIds(context.Request, routeConfig); 44 | var route = routeConfig.Route; 45 | var skipPayload = route.Use == "downstream" && SkipPayloadMethods.Contains(route.DownstreamMethod); 46 | var routeData = context.GetRouteData(); 47 | var hasTransformations = !skipPayload && _payloadTransformer.HasTransformations(resourceId, route); 48 | var payload = hasTransformations 49 | ? _payloadTransformer.Transform(await _payloadBuilder.BuildRawAsync(context.Request), 50 | resourceId, route, context.Request, routeData) 51 | : null; 52 | 53 | var executionData = new ExecutionData 54 | { 55 | RequestId = requestId, 56 | ResourceId = resourceId, 57 | TraceId = traceId, 58 | UserId = context.Request.HttpContext.User?.Identity?.Name, 59 | Claims = context.Request.HttpContext.User?.Claims?.ToDictionary(c => c.Type, c => c.Value) ?? 60 | EmptyClaims, 61 | ContentType = contentTypeValue, 62 | Route = routeConfig.Route, 63 | Context = context, 64 | Data = routeData, 65 | Downstream = _downstreamBuilder.GetDownstream(routeConfig, context.Request, routeData), 66 | Payload = payload?.Payload, 67 | HasPayload = hasTransformations 68 | }; 69 | 70 | if (skipPayload || payload is null) 71 | { 72 | return executionData; 73 | } 74 | 75 | executionData.ValidationErrors = await _payloadValidator.GetValidationErrorsAsync(payload); 76 | 77 | return executionData; 78 | } 79 | 80 | private (string, string, string) GenerateIds(HttpRequest request, RouteConfig routeConfig) 81 | { 82 | var requestId = string.Empty; 83 | var resourceId = string.Empty; 84 | var traceId = string.Empty; 85 | if (routeConfig.Route.GenerateRequestId == true || 86 | _options.GenerateRequestId == true && routeConfig.Route.GenerateRequestId != false) 87 | { 88 | requestId = Guid.NewGuid().ToString("N"); 89 | } 90 | 91 | if (!(request.Method is "GET" || request.Method is "DELETE") && 92 | (routeConfig.Route.ResourceId?.Generate == true || 93 | _options.ResourceId?.Generate == true && routeConfig.Route.ResourceId?.Generate != false)) 94 | { 95 | resourceId = Guid.NewGuid().ToString("N"); 96 | } 97 | 98 | if (routeConfig.Route.GenerateTraceId == true || 99 | _options.GenerateTraceId == true && routeConfig.Route.GenerateTraceId != false) 100 | { 101 | traceId = request.HttpContext.TraceIdentifier; 102 | } 103 | 104 | return (requestId, resourceId, traceId); 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/Ntrada/Requests/SchemaValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using NJsonSchema; 5 | 6 | namespace Ntrada.Requests 7 | { 8 | internal sealed class SchemaValidator : ISchemaValidator 9 | { 10 | public async Task> ValidateAsync(string payload, string schema) 11 | { 12 | if (string.IsNullOrWhiteSpace(schema)) 13 | { 14 | return Enumerable.Empty(); 15 | } 16 | 17 | var jsonSchema = await JsonSchema.FromJsonAsync(schema); 18 | var errors = jsonSchema.Validate(payload); 19 | 20 | return errors.Select(e => new Error 21 | { 22 | Code = e.Kind.ToString(), 23 | Property = e.Property, 24 | Message = e.ToString() 25 | }); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Ntrada/Requests/ValueProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Routing; 4 | 5 | namespace Ntrada.Requests 6 | { 7 | internal sealed class ValueProvider : IValueProvider 8 | { 9 | private static readonly string[] AvailableTokens = {"user_id"}; 10 | 11 | public IEnumerable Tokens => AvailableTokens; 12 | 13 | public string Get(string value, HttpRequest request, RouteData data) 14 | { 15 | switch ($"{value?.ToLowerInvariant()}") 16 | { 17 | case "@user_id": return request.HttpContext?.User?.Identity?.Name; 18 | default: return value; 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Ntrada/RouteConfig.cs: -------------------------------------------------------------------------------- 1 | using Ntrada.Configuration; 2 | 3 | namespace Ntrada 4 | { 5 | public class RouteConfig 6 | { 7 | public Route Route { get; set; } 8 | public string Downstream { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/Routing/DownstreamBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Routing; 4 | using Ntrada.Options; 5 | 6 | namespace Ntrada.Routing 7 | { 8 | internal sealed class DownstreamBuilder : IDownstreamBuilder 9 | { 10 | private readonly NtradaOptions _options; 11 | private readonly IValueProvider _valueProvider; 12 | 13 | public DownstreamBuilder(NtradaOptions options, IValueProvider valueProvider) 14 | { 15 | _options = options; 16 | _valueProvider = valueProvider; 17 | } 18 | 19 | public string GetDownstream(RouteConfig routeConfig, HttpRequest request, RouteData data) 20 | { 21 | if (string.IsNullOrWhiteSpace(routeConfig.Downstream)) 22 | { 23 | return null; 24 | } 25 | 26 | var stringBuilder = new StringBuilder(); 27 | var downstream = routeConfig.Downstream; 28 | stringBuilder.Append(downstream); 29 | if (downstream.Contains("@")) 30 | { 31 | foreach (var token in _valueProvider.Tokens) 32 | { 33 | var tokenName = $"@{token}"; 34 | stringBuilder.Replace(tokenName, _valueProvider.Get(tokenName, request, data)); 35 | } 36 | } 37 | 38 | foreach (var (key, value) in data.Values) 39 | { 40 | if (value is null) 41 | { 42 | continue; 43 | } 44 | 45 | if (key is "url") 46 | { 47 | stringBuilder.Append($"/{value}"); 48 | continue; 49 | } 50 | 51 | stringBuilder.Replace($"{{{key}}}", value.ToString()); 52 | } 53 | 54 | if (_options.PassQueryString == false || routeConfig.Route.PassQueryString == false) 55 | { 56 | return stringBuilder.ToString(); 57 | } 58 | 59 | var queryString = request.QueryString.ToString(); 60 | if (downstream.Contains("?") && !string.IsNullOrWhiteSpace(queryString)) 61 | { 62 | queryString = $"&{queryString.Substring(1, queryString.Length - 1)}"; 63 | } 64 | 65 | stringBuilder.Append(queryString); 66 | 67 | return stringBuilder.ToString(); 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/Ntrada/Routing/RequestExecutionValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace Ntrada.Routing 6 | { 7 | internal sealed class RequestExecutionValidator : IRequestExecutionValidator 8 | { 9 | private readonly IAuthenticationManager _authenticationManager; 10 | private readonly IAuthorizationManager _authorizationManager; 11 | private readonly ILogger _logger; 12 | 13 | public RequestExecutionValidator(IAuthenticationManager authenticationManager, 14 | IAuthorizationManager authorizationManager, ILogger logger) 15 | { 16 | _authenticationManager = authenticationManager; 17 | _authorizationManager = authorizationManager; 18 | _logger = logger; 19 | } 20 | 21 | public async Task TryExecuteAsync(HttpContext context, RouteConfig routeConfig) 22 | { 23 | var traceId = context.TraceIdentifier; 24 | var isAuthenticated = await _authenticationManager.TryAuthenticateAsync(context.Request, routeConfig); 25 | if (!isAuthenticated) 26 | { 27 | _logger.LogWarning($"Unauthorized request to: {routeConfig.Route.Upstream} [Trace ID: {traceId}]"); 28 | context.Response.StatusCode = 401; 29 | 30 | return false; 31 | } 32 | 33 | if (_authorizationManager.IsAuthorized(context.User, routeConfig)) 34 | { 35 | return true; 36 | } 37 | 38 | _logger.LogWarning($"Forbidden request to: {routeConfig.Route.Upstream} by user: " + 39 | $"{context.User.Identity.Name} [Trace ID: {traceId}]"); 40 | context.Response.StatusCode = 403; 41 | 42 | return false; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Ntrada/Routing/RouteConfigurator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Ntrada.Configuration; 3 | using Ntrada.Options; 4 | 5 | namespace Ntrada.Routing 6 | { 7 | internal sealed class RouteConfigurator : IRouteConfigurator 8 | { 9 | private readonly NtradaOptions _options; 10 | 11 | public RouteConfigurator(NtradaOptions options) 12 | { 13 | _options = options; 14 | } 15 | 16 | public RouteConfig Configure(Module module, Route route) 17 | => new RouteConfig 18 | { 19 | Route = route, 20 | Downstream = GetDownstream(module, route) 21 | }; 22 | 23 | private string GetDownstream(Module module, Route route) 24 | { 25 | if (string.IsNullOrWhiteSpace(route.Downstream)) 26 | { 27 | return null; 28 | } 29 | 30 | var loadBalancerEnabled = _options.LoadBalancer?.Enabled == true; 31 | var loadBalancerUrl = _options.LoadBalancer?.Url; 32 | if (loadBalancerEnabled) 33 | { 34 | if (string.IsNullOrWhiteSpace(loadBalancerUrl)) 35 | { 36 | throw new ArgumentException("Load balancer URL cannot be empty.", nameof(loadBalancerUrl)); 37 | } 38 | 39 | loadBalancerUrl = loadBalancerUrl.EndsWith("/") ? loadBalancerUrl : $"{loadBalancerUrl}/"; 40 | } 41 | 42 | var basePath = route.Downstream.Contains("/") 43 | ? route.Downstream.Split('/')[0] 44 | : route.Downstream; 45 | 46 | var hasService = module.Services.TryGetValue(basePath, out var service); 47 | if (!hasService) 48 | { 49 | return SetProtocol(route.Downstream); 50 | } 51 | 52 | if (service is null) 53 | { 54 | throw new ArgumentException($"Service for: '{basePath}' was not defined.", nameof(service.Url)); 55 | } 56 | 57 | if (_options.UseLocalUrl) 58 | { 59 | if (string.IsNullOrWhiteSpace(service.LocalUrl)) 60 | { 61 | throw new ArgumentException($"Local URL for: '{basePath}' cannot be empty if useLocalUrl = true.", 62 | nameof(service.LocalUrl)); 63 | } 64 | 65 | return SetProtocol(route.Downstream.Replace(basePath, service.LocalUrl)); 66 | } 67 | 68 | if (!string.IsNullOrWhiteSpace(service.LocalUrl) && string.IsNullOrWhiteSpace(service.Url)) 69 | { 70 | return SetProtocol(route.Downstream.Replace(basePath, service.LocalUrl)); 71 | } 72 | 73 | if (string.IsNullOrWhiteSpace(service.Url)) 74 | { 75 | throw new ArgumentException($"Service URL for: '{basePath}' cannot be empty.", nameof(service.Url)); 76 | } 77 | 78 | if (!loadBalancerEnabled) 79 | { 80 | return SetProtocol(route.Downstream.Replace(basePath, service.Url)); 81 | } 82 | 83 | var serviceUrl = service.Url.StartsWith("/") ? service.Url.Substring(1) : service.Url; 84 | var loadBalancedServiceUrl = $"{loadBalancerUrl}{serviceUrl}"; 85 | 86 | return SetProtocol(route.Downstream.Replace(basePath, loadBalancedServiceUrl)); 87 | } 88 | 89 | private static string SetProtocol(string service) => service.StartsWith("http") ? service : $"http://{service}"; 90 | } 91 | } -------------------------------------------------------------------------------- /src/Ntrada/Routing/RouteProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | using Microsoft.Extensions.Logging; 9 | using Ntrada.Options; 10 | using Ntrada.WebApi; 11 | 12 | namespace Ntrada.Routing 13 | { 14 | internal sealed class RouteProvider : IRouteProvider 15 | { 16 | private readonly IDictionary> _methods; 17 | private readonly IRouteConfigurator _routeConfigurator; 18 | private readonly IRequestExecutionValidator _requestExecutionValidator; 19 | private readonly IUpstreamBuilder _upstreamBuilder; 20 | private readonly WebApiEndpointDefinitions _definitions; 21 | private readonly NtradaOptions _options; 22 | private readonly IRequestHandlerManager _requestHandlerManager; 23 | private readonly ILogger _logger; 24 | 25 | public RouteProvider(NtradaOptions options, IRequestHandlerManager requestHandlerManager, 26 | IRouteConfigurator routeConfigurator, IRequestExecutionValidator requestExecutionValidator, 27 | IUpstreamBuilder upstreamBuilder, WebApiEndpointDefinitions definitions, ILogger logger) 28 | { 29 | _routeConfigurator = routeConfigurator; 30 | _requestExecutionValidator = requestExecutionValidator; 31 | _upstreamBuilder = upstreamBuilder; 32 | _definitions = definitions; 33 | _options = options; 34 | _requestHandlerManager = requestHandlerManager; 35 | _logger = logger; 36 | _methods = new Dictionary> 37 | { 38 | ["get"] = (builder, path, routeConfig) => 39 | builder.MapGet(path, ctx => Handle(ctx, routeConfig)), 40 | ["post"] = (builder, path, routeConfig) => 41 | builder.MapPost(path, ctx => Handle(ctx, routeConfig)), 42 | ["put"] = (builder, path, routeConfig) => 43 | builder.MapPut(path, ctx => Handle(ctx, routeConfig)), 44 | ["delete"] = (builder, path, routeConfig) => 45 | builder.MapDelete(path, ctx => Handle(ctx, routeConfig)), 46 | }; 47 | } 48 | 49 | private async Task Handle(HttpContext context, RouteConfig routeConfig) 50 | { 51 | var skipAuth = _options.Auth is null || 52 | !_options.Auth.Global && routeConfig.Route.Auth is null || 53 | routeConfig.Route.Auth == false; 54 | if (!skipAuth && 55 | !await _requestExecutionValidator.TryExecuteAsync(context, routeConfig)) 56 | { 57 | return; 58 | } 59 | 60 | var handler = routeConfig.Route.Use; 61 | await _requestHandlerManager.HandleAsync(handler, context, routeConfig); 62 | } 63 | 64 | public Action Build() => routeBuilder => 65 | { 66 | foreach (var module in _options.Modules.Where(m => m.Value.Enabled != false)) 67 | { 68 | _logger.LogInformation($"Building routes for the module: '{module.Key}'"); 69 | foreach (var route in module.Value.Routes) 70 | { 71 | if (string.IsNullOrWhiteSpace(route.Method) && route.Methods is null) 72 | { 73 | throw new ArgumentException("Both, route 'method' and 'methods' cannot be empty."); 74 | } 75 | 76 | route.Upstream = _upstreamBuilder.Build(module.Value, route); 77 | var routeConfig = _routeConfigurator.Configure(module.Value, route); 78 | 79 | if (!string.IsNullOrWhiteSpace(route.Method)) 80 | { 81 | _methods[route.Method](routeBuilder, route.Upstream, routeConfig); 82 | AddEndpointDefinition(route.Method, route.Upstream); 83 | } 84 | 85 | if (route.Methods is null) 86 | { 87 | continue; 88 | } 89 | 90 | foreach (var method in route.Methods) 91 | { 92 | var methodType = method.ToLowerInvariant(); 93 | _methods[methodType](routeBuilder, route.Upstream, routeConfig); 94 | AddEndpointDefinition(methodType, route.Upstream); 95 | } 96 | } 97 | } 98 | }; 99 | 100 | private void AddEndpointDefinition(string method, string path) 101 | { 102 | if (string.IsNullOrWhiteSpace(path)) 103 | { 104 | path = "/"; 105 | } 106 | 107 | _definitions.Add(new WebApiEndpointDefinition 108 | { 109 | Method = method, 110 | Path = path, 111 | Responses = new List 112 | { 113 | new WebApiEndpointResponse 114 | { 115 | StatusCode = 200 116 | } 117 | } 118 | }); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /src/Ntrada/Routing/UpstreamBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.Extensions.Logging; 3 | using Ntrada.Configuration; 4 | using Ntrada.Options; 5 | 6 | namespace Ntrada.Routing 7 | { 8 | internal sealed class UpstreamBuilder : IUpstreamBuilder 9 | { 10 | private readonly NtradaOptions _options; 11 | private readonly IRequestHandlerManager _requestHandlerManager; 12 | private readonly ILogger _logger; 13 | 14 | public UpstreamBuilder(NtradaOptions options, IRequestHandlerManager requestHandlerManager, 15 | ILogger logger) 16 | { 17 | _options = options; 18 | _requestHandlerManager = requestHandlerManager; 19 | _logger = logger; 20 | } 21 | 22 | public string Build(Module module, Route route) 23 | { 24 | var path = module.Path; 25 | var upstream = string.IsNullOrWhiteSpace(route.Upstream) ? string.Empty : route.Upstream; 26 | if (!string.IsNullOrWhiteSpace(path)) 27 | { 28 | var modulePath = path.EndsWith("/") ? path.Substring(0, path.Length - 1) : path; 29 | if (!upstream.StartsWith("/")) 30 | { 31 | upstream = $"/{upstream}"; 32 | } 33 | 34 | upstream = $"{modulePath}{upstream}"; 35 | } 36 | 37 | if (upstream.EndsWith("/")) 38 | { 39 | upstream = upstream.Substring(0, upstream.Length - 1); 40 | } 41 | 42 | if (route.MatchAll) 43 | { 44 | upstream = $"{upstream}/{{*url}}"; 45 | } 46 | 47 | var handler = _requestHandlerManager.Get(route.Use); 48 | var routeInfo = handler.GetInfo(route); 49 | var isPublicInfo = _options.Auth is null || !_options.Auth.Global && route.Auth is null || 50 | route.Auth == false 51 | ? "public" 52 | : "protected"; 53 | 54 | var methods = new HashSet(); 55 | if (!string.IsNullOrWhiteSpace(route.Method)) 56 | { 57 | methods.Add(route.Method.ToUpperInvariant()); 58 | } 59 | 60 | if (route.Methods is {}) 61 | { 62 | foreach (var method in route.Methods) 63 | { 64 | if (string.IsNullOrWhiteSpace(method)) 65 | { 66 | continue; 67 | } 68 | 69 | methods.Add(method.ToUpperInvariant()); 70 | } 71 | } 72 | 73 | _logger.LogInformation($"Added {isPublicInfo} route for upstream: [{string.Join(", ", methods)}] " + 74 | $"'{upstream}' -> {routeInfo}"); 75 | 76 | return upstream; 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/Ntrada/WebApi/WebApiEndpointDefinition.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada.WebApi 4 | { 5 | public class WebApiEndpointDefinition 6 | { 7 | public string Method { get; set; } 8 | public string Path { get; set; } 9 | public IEnumerable Parameters { get; set; } = new List(); 10 | public IEnumerable Responses { get; set; } = new List(); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Ntrada/WebApi/WebApiEndpointDefinitions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Ntrada.WebApi 4 | { 5 | public class WebApiEndpointDefinitions : List 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /src/Ntrada/WebApi/WebApiEndpointParameter.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.WebApi 2 | { 3 | public class WebApiEndpointParameter 4 | { 5 | public string In { get; set; } 6 | public string Type { get; set; } 7 | public string Name { get; set; } 8 | public object Example { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Ntrada/WebApi/WebApiEndpointResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Ntrada.WebApi 2 | { 3 | public class WebApiEndpointResponse 4 | { 5 | public string Type { get; set; } 6 | public int StatusCode { get; set; } 7 | public object Example { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /tests/Ntrada.Tests.Integration/Ntrada.Tests.Integration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | latest 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/Ntrada.Tests.Unit/Auth/AuthenticationManagerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Security.Claims; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authentication; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using NSubstitute; 9 | using Ntrada.Auth; 10 | using Ntrada.Configuration; 11 | using Ntrada.Options; 12 | using Shouldly; 13 | using Xunit; 14 | 15 | namespace Ntrada.Tests.Unit.Auth 16 | { 17 | [ExcludeFromCodeCoverage] 18 | public class AuthenticationManagerTests 19 | { 20 | private Task Act() => _authenticationManager.TryAuthenticateAsync(_httpContext.Request, _routeConfig); 21 | 22 | [Fact] 23 | public async Task try_authenticate_should_return_true_if_auth_is_disabled() 24 | { 25 | var result = await Act(); 26 | result.ShouldBeTrue(); 27 | await _authenticationService.DidNotReceiveWithAnyArgs().AuthenticateAsync(null, null); 28 | } 29 | 30 | [Fact] 31 | public async Task try_authenticate_should_return_true_if_global_auth_is_disabled() 32 | { 33 | _options.Auth = new Configuration.Auth 34 | { 35 | Enabled = true 36 | }; 37 | var result = await Act(); 38 | result.ShouldBeTrue(); 39 | await _authenticationService.DidNotReceiveWithAnyArgs().AuthenticateAsync(null, null); 40 | } 41 | 42 | [Fact] 43 | public async Task try_authenticate_should_return_true_if_route_auth_is_disabled() 44 | { 45 | _options.Auth = new Configuration.Auth 46 | { 47 | Enabled = true 48 | }; 49 | _routeConfig.Route = new Route 50 | { 51 | Auth = false 52 | }; 53 | var result = await Act(); 54 | result.ShouldBeTrue(); 55 | await _authenticationService.DidNotReceiveWithAnyArgs().AuthenticateAsync(null, null); 56 | } 57 | 58 | [Fact] 59 | public async Task try_authenticate_should_return_false_if_global_auth_is_enabled_and_user_is_not_authenticated() 60 | { 61 | _options.Auth = new Configuration.Auth 62 | { 63 | Enabled = true, 64 | Global = true 65 | }; 66 | SetFailedAuth(); 67 | var result = await Act(); 68 | result.ShouldBeFalse(); 69 | await _authenticationService.Received().AuthenticateAsync(Arg.Any(), Arg.Any()); 70 | } 71 | 72 | [Fact] 73 | public async Task try_authenticate_should_return_false_if_route_auth_is_enabled_and_user_is_not_authenticated() 74 | { 75 | _options.Auth = new Configuration.Auth 76 | { 77 | Enabled = true, 78 | Global = false 79 | }; 80 | _routeConfig.Route = new Route 81 | { 82 | Auth = true 83 | }; 84 | SetFailedAuth(); 85 | var result = await Act(); 86 | result.ShouldBeFalse(); 87 | await _authenticationService.Received().AuthenticateAsync(_httpContext, Arg.Any()); 88 | } 89 | 90 | [Fact] 91 | public async Task try_authenticate_should_return_true_if_global_auth_is_enabled_and_user_is_authenticated() 92 | { 93 | _options.Auth = new Configuration.Auth 94 | { 95 | Enabled = true, 96 | Global = true 97 | }; 98 | SetSuccessfulAuth(); 99 | var result = await Act(); 100 | result.ShouldBeTrue(); 101 | await _authenticationService.Received().AuthenticateAsync(_httpContext, Arg.Any()); 102 | } 103 | 104 | [Fact] 105 | public async Task try_authenticate_should_return_true_if_route_auth_is_enabled_and_user_is_authenticated() 106 | { 107 | _options.Auth = new Configuration.Auth 108 | { 109 | Enabled = true, 110 | Global = false 111 | }; 112 | _routeConfig.Route = new Route 113 | { 114 | Auth = true 115 | }; 116 | SetSuccessfulAuth(); 117 | var result = await Act(); 118 | result.ShouldBeTrue(); 119 | await _authenticationService.Received().AuthenticateAsync(_httpContext, Arg.Any()); 120 | } 121 | 122 | #region Arrange 123 | 124 | private readonly IAuthenticationManager _authenticationManager; 125 | private readonly NtradaOptions _options; 126 | private readonly HttpContext _httpContext; 127 | private readonly RouteConfig _routeConfig; 128 | private readonly IAuthenticationService _authenticationService; 129 | private const string Scheme = "test"; 130 | 131 | public AuthenticationManagerTests() 132 | { 133 | _options = new NtradaOptions(); 134 | var serviceProvider = Substitute.For(); 135 | serviceProvider.GetService(typeof(IAuthenticationService)).Returns(_authenticationService); 136 | _authenticationService = Substitute.For(); 137 | _httpContext = new DefaultHttpContext 138 | { 139 | RequestServices = new ServiceProviderStub(_authenticationService), 140 | }; 141 | _routeConfig = new RouteConfig(); 142 | _authenticationManager = new AuthenticationManager(_options); 143 | } 144 | 145 | private void SetSuccessfulAuth() 146 | { 147 | _authenticationService.AuthenticateAsync(Arg.Any(), Arg.Any()) 148 | .Returns(AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(), Scheme))); 149 | } 150 | 151 | private void SetFailedAuth() 152 | { 153 | _authenticationService.AuthenticateAsync(Arg.Any(), Arg.Any()) 154 | .Returns(AuthenticateResult.Fail(Scheme)); 155 | } 156 | 157 | private class ServiceProviderStub : IServiceProvider, ISupportRequiredService 158 | { 159 | private readonly IAuthenticationService _authenticationService; 160 | 161 | public ServiceProviderStub(IAuthenticationService authenticationService) 162 | { 163 | _authenticationService = authenticationService; 164 | } 165 | 166 | public object GetService(Type serviceType) 167 | => serviceType == typeof(IAuthenticationService) ? _authenticationService : null; 168 | 169 | public object GetRequiredService(Type serviceType) 170 | => serviceType == typeof(IAuthenticationService) ? _authenticationService : null; 171 | } 172 | 173 | #endregion 174 | } 175 | } -------------------------------------------------------------------------------- /tests/Ntrada.Tests.Unit/Auth/AuthorizationManagerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using NSubstitute; 6 | using Ntrada.Auth; 7 | using Ntrada.Configuration; 8 | using Shouldly; 9 | using Xunit; 10 | 11 | namespace Ntrada.Tests.Unit.Auth 12 | { 13 | [ExcludeFromCodeCoverage] 14 | public class AuthorizationManagerTests 15 | { 16 | private bool Act() => _authorizationManager.IsAuthorized(_user, _routeConfig); 17 | 18 | [Fact] 19 | public void is_authorized_should_return_false_if_user_is_null() 20 | { 21 | _user = null; 22 | var result = Act(); 23 | result.ShouldBeFalse(); 24 | _policyManager.DidNotReceiveWithAnyArgs().GetClaims(null); 25 | } 26 | 27 | [Fact] 28 | public void is_authorized_should_return_true_if_route_config_route_and_claims_are_null() 29 | { 30 | var result = Act(); 31 | result.ShouldBeTrue(); 32 | _policyManager.DidNotReceiveWithAnyArgs().GetClaims(null); 33 | } 34 | 35 | [Fact] 36 | public void is_authorized_should_return_true_if_route_config_route_policies_is_null() 37 | { 38 | _routeConfig.Route = new Route(); 39 | var result = Act(); 40 | result.ShouldBeTrue(); 41 | _policyManager.DidNotReceiveWithAnyArgs().GetClaims(null); 42 | } 43 | 44 | [Fact] 45 | public void is_authorized_should_return_true_if_route_config_route_claims_is_null() 46 | { 47 | _routeConfig.Route = new Route 48 | { 49 | Policies = Enumerable.Empty() 50 | }; 51 | var result = Act(); 52 | result.ShouldBeTrue(); 53 | _policyManager.DidNotReceiveWithAnyArgs().GetClaims(null); 54 | } 55 | 56 | [Fact] 57 | public void is_authorized_should_return_false_if_user_has_no_policies() 58 | { 59 | _routeConfig.Route = new Route 60 | { 61 | Policies = _policies 62 | }; 63 | var result = Act(); 64 | result.ShouldBeFalse(); 65 | _policyManager.Received().GetClaims(_policies.ElementAt(0)); 66 | foreach (var policy in _policies.Skip(1)) 67 | { 68 | _policyManager.DidNotReceive().GetClaims(policy); 69 | } 70 | } 71 | 72 | [Fact] 73 | public void is_authorized_should_return_false_if_user_has_some_policies() 74 | { 75 | _routeConfig.Route = new Route 76 | { 77 | Policies = _policies 78 | }; 79 | SetClaimsIdentity(skipClaims: 1); 80 | var result = Act(); 81 | result.ShouldBeFalse(); 82 | _policyManager.Received().GetClaims(_policies.ElementAt(0)); 83 | foreach (var policy in _policies.Skip(1)) 84 | { 85 | _policyManager.DidNotReceive().GetClaims(policy); 86 | } 87 | } 88 | 89 | [Fact] 90 | public void is_authorized_should_return_true_if_user_has_all_policies() 91 | { 92 | _routeConfig.Route = new Route 93 | { 94 | Policies = _policies 95 | }; 96 | SetClaimsIdentity(); 97 | var result = Act(); 98 | result.ShouldBeTrue(); 99 | foreach (var policy in _policies) 100 | { 101 | _policyManager.Received().GetClaims(policy); 102 | } 103 | } 104 | 105 | [Fact] 106 | public void is_authorized_should_return_false_if_user_has_no_claims() 107 | { 108 | _routeConfig.Route = new Route 109 | { 110 | Claims = _claims 111 | }; 112 | var result = Act(); 113 | result.ShouldBeFalse(); 114 | _policyManager.DidNotReceiveWithAnyArgs().GetClaims(null); 115 | } 116 | 117 | [Fact] 118 | public void is_authorized_should_return_false_if_user_has_some_claims() 119 | { 120 | _routeConfig.Route = new Route 121 | { 122 | Claims = _claims 123 | }; 124 | SetClaimsIdentity(skipClaims: 1); 125 | var result = Act(); 126 | result.ShouldBeFalse(); 127 | _policyManager.DidNotReceiveWithAnyArgs().GetClaims(null); 128 | } 129 | 130 | [Fact] 131 | public void is_authorized_should_return_true_if_user_has_all_claims() 132 | { 133 | _routeConfig.Route = new Route 134 | { 135 | Claims = _claims 136 | }; 137 | SetClaimsIdentity(); 138 | var result = Act(); 139 | result.ShouldBeTrue(); 140 | _policyManager.DidNotReceiveWithAnyArgs().GetClaims(null); 141 | } 142 | 143 | [Fact] 144 | public void is_authorized_should_return_true_if_user_has_all_claims_and_all_policies() 145 | { 146 | _routeConfig.Route = new Route 147 | { 148 | Claims = _claims, 149 | Policies = _policies 150 | }; 151 | SetClaimsIdentity(); 152 | var result = Act(); 153 | result.ShouldBeTrue(); 154 | foreach (var policy in _policies) 155 | { 156 | _policyManager.Received().GetClaims(policy); 157 | } 158 | } 159 | 160 | [Fact] 161 | public void is_authorized_should_return_true_if_user_has_some_claims_and_some_policies() 162 | { 163 | _routeConfig.Route = new Route 164 | { 165 | Claims = _claims, 166 | Policies = _policies 167 | }; 168 | SetClaimsIdentity(1); 169 | var result = Act(); 170 | result.ShouldBeFalse(); 171 | _policyManager.Received().GetClaims(_policies.ElementAt(0)); 172 | foreach (var policy in _policies.Skip(1)) 173 | { 174 | _policyManager.DidNotReceive().GetClaims(policy); 175 | } 176 | } 177 | 178 | #region Arrange 179 | 180 | private readonly IEnumerable _policies = new[] {"policy1", "policy2", "policy3"}; 181 | 182 | private readonly IDictionary _claims = new Dictionary 183 | { 184 | ["claim1"] = "value1", 185 | ["claim2"] = "value2", 186 | ["claim3"] = "value3" 187 | }; 188 | 189 | private readonly IAuthorizationManager _authorizationManager; 190 | private readonly IPolicyManager _policyManager; 191 | private ClaimsPrincipal _user; 192 | private readonly RouteConfig _routeConfig; 193 | 194 | public AuthorizationManagerTests() 195 | { 196 | _policyManager = Substitute.For(); 197 | foreach (var policy in _policies) 198 | { 199 | _policyManager.GetClaims(policy).Returns(_claims); 200 | } 201 | 202 | _user = new ClaimsPrincipal(); 203 | _routeConfig = new RouteConfig(); 204 | _authorizationManager = new AuthorizationManager(_policyManager); 205 | } 206 | 207 | private void SetClaimsIdentity(int skipClaims = 0) 208 | => _user = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(GetClaims().Skip(skipClaims)))); 209 | 210 | private IEnumerable GetClaims() => _claims.Select(c => new Claim(c.Key, c.Value)); 211 | 212 | #endregion 213 | } 214 | } -------------------------------------------------------------------------------- /tests/Ntrada.Tests.Unit/Auth/PolicyManagerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using Ntrada.Auth; 5 | using Ntrada.Configuration; 6 | using Ntrada.Options; 7 | using Shouldly; 8 | using Xunit; 9 | 10 | namespace Ntrada.Tests.Unit.Auth 11 | { 12 | [ExcludeFromCodeCoverage] 13 | public class PolicyManagerTests 14 | { 15 | private IDictionary Act(string policy) => _policyManager.GetClaims(policy); 16 | 17 | [Fact] 18 | public void get_claims_should_be_null_if_policies_are_not_defined() 19 | { 20 | var result = Act(Policy1); 21 | result.ShouldBeNull(); 22 | } 23 | 24 | [Fact] 25 | public void get_claims_should_be_null_if_policy_does_not_exist() 26 | { 27 | _options.Auth = new Configuration.Auth 28 | { 29 | Policies = _policies 30 | }; 31 | _policyManager = new PolicyManager(_options); 32 | var result = Act(MissingPolicy); 33 | result.ShouldBeNull(); 34 | } 35 | 36 | [Fact] 37 | public void get_claims_should_not_be_empty_if_policy_exists() 38 | { 39 | _options.Auth = new Configuration.Auth 40 | { 41 | Policies = _policies 42 | }; 43 | _policyManager = new PolicyManager(_options); 44 | var result = Act(Policy1); 45 | result.ShouldNotBeEmpty(); 46 | } 47 | 48 | [Fact] 49 | public void get_claims_should_not_be_empty_if_policy_exists_and_is_used_in_routes() 50 | { 51 | _options.Auth = new Configuration.Auth 52 | { 53 | Policies = _policies 54 | }; 55 | SetModulesWithPolicies(new[] {Policy1, Policy2}); 56 | _policyManager = new PolicyManager(_options); 57 | var result = Act(Policy1); 58 | result.ShouldNotBeEmpty(); 59 | } 60 | 61 | [Fact] 62 | public void get_claims_should_throw_an_exception_if_policy_used_in_route_was_not_defined() 63 | { 64 | _options.Auth = new Configuration.Auth 65 | { 66 | Policies = _policies 67 | }; 68 | SetModulesWithPolicies(new[] {Policy1, Policy2, MissingPolicy}); 69 | var exception = Record.Exception(() => _policyManager = new PolicyManager(_options)); 70 | exception.ShouldNotBeNull(); 71 | exception.ShouldBeOfType(); 72 | } 73 | 74 | #region Arrange 75 | 76 | private const string Policy1 = "policy1"; 77 | private const string Policy2 = "policy2"; 78 | private const string MissingPolicy = "policy3"; 79 | 80 | private readonly IDictionary _policies = 81 | new Dictionary 82 | { 83 | [Policy1] = new Policy 84 | { 85 | Claims = new Dictionary 86 | { 87 | ["claim1"] = "value1" 88 | } 89 | }, 90 | [Policy2] = new Policy 91 | { 92 | Claims = new Dictionary 93 | { 94 | ["claim2"] = "value2", 95 | ["claim3"] = "value3" 96 | } 97 | } 98 | }; 99 | 100 | private IPolicyManager _policyManager; 101 | private NtradaOptions _options; 102 | 103 | public PolicyManagerTests() 104 | { 105 | _options = new NtradaOptions(); 106 | _policyManager = new PolicyManager(_options); 107 | } 108 | 109 | private void SetModulesWithPolicies(IEnumerable policies) 110 | { 111 | _options.Modules = new Dictionary 112 | { 113 | ["module1"] = new Module 114 | { 115 | Routes = new[] 116 | { 117 | new Route 118 | { 119 | Policies = policies 120 | } 121 | } 122 | } 123 | }; 124 | } 125 | 126 | #endregion 127 | } 128 | } -------------------------------------------------------------------------------- /tests/Ntrada.Tests.Unit/Extensions/ExtensionProviderTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Ntrada.Extensions; 7 | using Ntrada.Options; 8 | using Shouldly; 9 | using Xunit; 10 | 11 | namespace Ntrada.Tests.Unit.Extensions 12 | { 13 | [ExcludeFromCodeCoverage] 14 | public class ExtensionProviderTests 15 | { 16 | private IEnumerable Act() => _extensionProvider.GetAll(); 17 | 18 | [Fact] 19 | public void get_all_should_return_empty_collection_if_extensions_are_not_defined() 20 | { 21 | var result = Act(); 22 | result.ShouldBeEmpty(); 23 | } 24 | 25 | [Fact] 26 | public void get_all_should_return_not_empty_collection_if_at_least_one_extension_was_defined() 27 | { 28 | _options.Extensions = new Dictionary 29 | { 30 | [ExtensionName] = new ExtensionOptions(), 31 | }; 32 | 33 | var result = Act(); 34 | result.ShouldNotBeEmpty(); 35 | } 36 | 37 | [Fact] 38 | public void get_all_should_return_extension_with_not_null_extension_property() 39 | { 40 | var options = new ExtensionOptions 41 | { 42 | Enabled = true, 43 | Order = 1 44 | }; 45 | _options.Extensions = new Dictionary 46 | { 47 | [ExtensionName] = options, 48 | }; 49 | 50 | var result = Act(); 51 | var extension = result.SingleOrDefault(); 52 | extension.ShouldNotBeNull(); 53 | extension.Extension.ShouldNotBeNull(); 54 | } 55 | 56 | [Fact] 57 | public void get_all_should_return_extension_with_the_same_options_property() 58 | { 59 | var options = new ExtensionOptions 60 | { 61 | Enabled = true, 62 | Order = 1 63 | }; 64 | _options.Extensions = new Dictionary 65 | { 66 | [ExtensionName] = options, 67 | }; 68 | 69 | var result = Act(); 70 | var extension = result.SingleOrDefault(); 71 | extension.ShouldNotBeNull(); 72 | extension.Options.Enabled.ShouldBe(options.Enabled); 73 | extension.Options.Order.ShouldBe(options.Order); 74 | } 75 | 76 | [Fact] 77 | public void get_all_should_return_the_same_extensions_if_invoked_twice() 78 | { 79 | _options.Extensions = new Dictionary 80 | { 81 | [ExtensionName] = new ExtensionOptions(), 82 | }; 83 | 84 | var result = Act(); 85 | var result2 = Act(); 86 | result.ShouldBeSameAs(result2); 87 | } 88 | 89 | #region Arrange 90 | 91 | private const string ExtensionName = "test"; 92 | private readonly IExtensionProvider _extensionProvider; 93 | private readonly NtradaOptions _options; 94 | 95 | public ExtensionProviderTests() 96 | { 97 | _options = new NtradaOptions(); 98 | _extensionProvider = new ExtensionProvider(_options); 99 | } 100 | 101 | private class TestExtension : IExtension 102 | { 103 | public string Name { get; } = ExtensionName; 104 | public string Description { get; } = ExtensionName; 105 | 106 | public void Add(IServiceCollection services, IOptionsProvider optionsProvider) 107 | { 108 | } 109 | 110 | public void Use(IApplicationBuilder app, IOptionsProvider optionsProvider) 111 | { 112 | } 113 | } 114 | 115 | #endregion 116 | } 117 | } -------------------------------------------------------------------------------- /tests/Ntrada.Tests.Unit/Handlers/HandlerTestsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Routing; 6 | using NSubstitute; 7 | 8 | namespace Ntrada.Tests.Unit.Handlers 9 | { 10 | [ExcludeFromCodeCoverage] 11 | public abstract class HandlerTestsBase 12 | { 13 | protected Task Act() => Handler.HandleAsync(HttpContext, RouteConfig); 14 | 15 | protected abstract void get_info_should_return_value(); 16 | 17 | #region Arrange 18 | 19 | protected readonly HttpContext HttpContext; 20 | protected readonly RouteConfig RouteConfig; 21 | protected readonly RouteData RouteData; 22 | protected readonly IRequestProcessor RequestProcessor; 23 | protected readonly IServiceProvider ServiceProvider; 24 | protected IHandler Handler; 25 | 26 | protected HandlerTestsBase() 27 | { 28 | HttpContext = new DefaultHttpContext(); 29 | RouteConfig = new RouteConfig(); 30 | RouteData = new RouteData(); 31 | RequestProcessor = Substitute.For(); 32 | ServiceProvider = Substitute.For(); 33 | InitHandler(); 34 | } 35 | 36 | protected abstract void InitHandler(); 37 | 38 | #endregion 39 | } 40 | } -------------------------------------------------------------------------------- /tests/Ntrada.Tests.Unit/Handlers/ReturnValueHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Threading.Tasks; 3 | using Ntrada.Configuration; 4 | using Ntrada.Handlers; 5 | using Shouldly; 6 | using Xunit; 7 | 8 | namespace Ntrada.Tests.Unit.Handlers 9 | { 10 | [ExcludeFromCodeCoverage] 11 | public class ReturnValueHandlerTests : HandlerTestsBase 12 | { 13 | [Fact] 14 | public async Task handle_should_write_return_value() 15 | { 16 | await Act(); 17 | } 18 | 19 | [Fact] 20 | protected override void get_info_should_return_value() 21 | { 22 | RouteConfig.Route = new Route 23 | { 24 | ReturnValue = "test" 25 | }; 26 | var info = Handler.GetInfo(RouteConfig.Route); 27 | info.ShouldBe($"return a value: '{RouteConfig.Route.ReturnValue}'"); 28 | } 29 | 30 | #region Arrange 31 | 32 | protected override void InitHandler() 33 | { 34 | Handler = new ReturnValueHandler(RequestProcessor, ServiceProvider); 35 | } 36 | 37 | #endregion 38 | } 39 | } -------------------------------------------------------------------------------- /tests/Ntrada.Tests.Unit/Ntrada.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | latest 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | --------------------------------------------------------------------------------