├── .gitattributes ├── .gitignore ├── LICENSE ├── LimitsMiddleware.sln ├── README.md ├── appveyor.yml ├── build ├── build.ps1 ├── cover.ps1 └── test.ps1 ├── demo └── LimitsMiddleware.AspNetCore.Demo │ ├── App_Packages │ └── LibLog.4.2 │ │ └── LibLog.cs │ ├── LimitsMiddleware.Demo.xproj │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Startup.cs │ ├── project.json │ └── web.config ├── global.json ├── src └── LimitsMiddleware.AspNetCore │ ├── App_Packages │ └── LibLog.4.2 │ │ └── LibLog.cs │ ├── ContentLengthExceededException.cs │ ├── ContentLengthLimitingStream.cs │ ├── ContentLengthRequiredException.cs │ ├── Extensions │ ├── ApplicationBuilderExtensions.ConnectionTimeout.cs │ ├── ApplicationBuilderExtensions.MaxBandwidthGlobal.cs │ ├── ApplicationBuilderExtensions.MaxBandwidthPerRequest.cs │ ├── ApplicationBuilderExtensions.MaxConcurrentRequests.cs │ ├── ApplicationBuilderExtensions.MaxQueryStringLength.cs │ ├── ApplicationBuilderExtensions.MaxRequestContentLength.cs │ ├── ApplicationBuilderExtensions.MaxUrlLength.cs │ └── ApplicationBuilderExtensions.MinResponseDelay.cs │ ├── InterlockedBoolean.cs │ ├── InterlockedBooleanExtensions.cs │ ├── Limits.ConnectionTimeout.cs │ ├── Limits.MaxBandwidthGlobal.cs │ ├── Limits.MaxBandwidthPerRequest.cs │ ├── Limits.MaxConcurrentRequests.cs │ ├── Limits.MaxQueryStringLength.cs │ ├── Limits.MaxRequestContentLength.cs │ ├── Limits.MaxUrlLength.cs │ ├── Limits.MinResponseDelay.cs │ ├── LimitsMiddleware.AspNetCore.xproj │ ├── LimitsMiddleware.xproj │ ├── Properties │ └── AssemblyInfo.cs │ ├── RateLimiters │ ├── FixedTokenBucket.cs │ ├── GetUtcNow.cs │ └── SystemClock.cs │ ├── RequestContext.cs │ ├── ThrottledStream.cs │ ├── TimeoutStream.cs │ └── project.json └── test └── LimitsMiddleware.AspNetCore.Tests ├── ConnectionTimeoutTests.cs ├── Files ├── file_512KB.txt └── file_64KB.txt ├── Helpers ├── EapTask.cs └── TaskExt.cs ├── HttpResponseExtensions.cs ├── LimitsMiddleware.AspNetCore.Tests.xproj ├── MaxBandwidthGlobalTests.cs ├── MaxBandwidthPerRequestTests.cs ├── MaxConcurrentRequestsTests.cs ├── MaxQueryStringTests.cs ├── MaxRequestContentLengthTests.cs ├── MaxUrlLengthTests.cs ├── MinResponseDelayMiddlewareTests.cs ├── Properties └── AssemblyInfo.cs ├── RateLimiters └── FixedTokenBucketTests.cs ├── project.json └── xunit.runner.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files we want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.c text 7 | *.h text 8 | *.cs text 9 | *.config text 10 | *.xml text 11 | *.manifest text 12 | *.bat text 13 | *.cmd text 14 | *.sh text 15 | *.txt text 16 | *.dat text 17 | *.rc text 18 | *.ps1 text 19 | *.psm1 text 20 | *.js text 21 | *.css text 22 | *.html text 23 | *.sln text 24 | *.DotSettings text 25 | *.csproj text 26 | *.ncrunchproject text 27 | *.fs text 28 | *.fsproj text 29 | *.liquid text 30 | *.boo text 31 | *.pp text 32 | *.targets text 33 | *.markdown text 34 | *.md text 35 | *.bat text 36 | *.xslt text 37 | 38 | # Declare files that will always have CRLF line endings on checkout. 39 | 40 | # Denote all files that are truly binary and should not be modified. 41 | *.ico binary 42 | *.gif binary 43 | *.png binary 44 | *.jpg binary 45 | *.dll binary 46 | *.exe binary 47 | *.pdb binary 48 | -------------------------------------------------------------------------------- /.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 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Luk Vermeulen 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 | -------------------------------------------------------------------------------- /LimitsMiddleware.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 14 3 | VisualStudioVersion = 14.0.25420.1 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2E129E87-1111-4E18-A73E-E64A2DA359BE}" 6 | ProjectSection(SolutionItems) = preProject 7 | ..\.gitattributes = ..\.gitattributes 8 | ..\.gitignore = ..\.gitignore 9 | appveyor.yml = appveyor.yml 10 | global.json = global.json 11 | LICENSE = LICENSE 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{248F98EE-CEF1-4190-8020-DFA2174ED82E}" 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Demo", "Demo", "{9F696FF8-7F23-4CB3-A875-1256F09EDBD5}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{50AB4FEC-B1AC-45F8-B7EE-E636F3619AAD}" 20 | ProjectSection(SolutionItems) = preProject 21 | build\build.ps1 = build\build.ps1 22 | build\cover.ps1 = build\cover.ps1 23 | build\test.ps1 = build\test.ps1 24 | EndProjectSection 25 | EndProject 26 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LimitsMiddleware.AspNetCore", "src\LimitsMiddleware.AspNetCore\LimitsMiddleware.AspNetCore.xproj", "{A61B64D6-3D62-472E-B9BA-2435166C6786}" 27 | EndProject 28 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LimitsMiddleware.AspNetCore.Tests", "test\LimitsMiddleware.AspNetCore.Tests\LimitsMiddleware.AspNetCore.Tests.xproj", "{1E42D5F7-31EB-4C2B-AFD6-841D2024E636}" 29 | EndProject 30 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LimitsMiddleware.Demo", "demo\LimitsMiddleware.AspNetCore.Demo\LimitsMiddleware.Demo.xproj", "{962B06A8-4D78-4438-878D-34A0F86F2A5C}" 31 | EndProject 32 | Global 33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 34 | Debug|Any CPU = Debug|Any CPU 35 | Release|Any CPU = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {A61B64D6-3D62-472E-B9BA-2435166C6786}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {A61B64D6-3D62-472E-B9BA-2435166C6786}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {A61B64D6-3D62-472E-B9BA-2435166C6786}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {A61B64D6-3D62-472E-B9BA-2435166C6786}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {1E42D5F7-31EB-4C2B-AFD6-841D2024E636}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {1E42D5F7-31EB-4C2B-AFD6-841D2024E636}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {1E42D5F7-31EB-4C2B-AFD6-841D2024E636}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {1E42D5F7-31EB-4C2B-AFD6-841D2024E636}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {962B06A8-4D78-4438-878D-34A0F86F2A5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {962B06A8-4D78-4438-878D-34A0F86F2A5C}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {962B06A8-4D78-4438-878D-34A0F86F2A5C}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {962B06A8-4D78-4438-878D-34A0F86F2A5C}.Release|Any CPU.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(NestedProjects) = preSolution 55 | {1E42D5F7-31EB-4C2B-AFD6-841D2024E636} = {248F98EE-CEF1-4190-8020-DFA2174ED82E} 56 | {962B06A8-4D78-4438-878D-34A0F86F2A5C} = {9F696FF8-7F23-4CB3-A875-1256F09EDBD5} 57 | EndGlobalSection 58 | EndGlobal 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Icon](http://i.imgur.com/D7PUb42.png?1) 2 | # Limits Middleware for ASP.NET Core [![Build status](https://ci.appveyor.com/api/projects/status/5oudipjv2e65bl1w?svg=true)](https://ci.appveyor.com/project/lvermeulen/limitsmiddleware-aspnetcore) [![license](https://img.shields.io/github/license/lvermeulen/LimitsMiddleware.aspnetcore.svg?maxAge=2592000)](https://github.com/lvermeulen/LimitsMiddleware.aspnetcore/blob/master/LICENSE) [![NuGet](https://img.shields.io/nuget/vpre/LimitsMiddleware.aspnetcore.svg?maxAge=2592000)](https://www.nuget.org/packages/LimitsMiddleware.aspnetcore/) [![Coverage Status](https://coveralls.io/repos/github/lvermeulen/LimitsMiddleware.aspnetcore/badge.svg?branch=master)](https://coveralls.io/github/lvermeulen/LimitsMiddleware.aspnetcore?branch=master) [![Join the chat at https://gitter.im/lvermeulen/LimitsMiddleware.aspnetcore](https://badges.gitter.im/lvermeulen/LimitsMiddleware.aspnetcore.svg)](https://gitter.im/lvermeulen/LimitsMiddleware.aspnetcore?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![](https://img.shields.io/badge/.net-4.5.1-yellowgreen.svg) ![](https://img.shields.io/badge/netstandard-1.6-yellowgreen.svg) 3 | Middleware to apply limits to an ASP.NET Core pipeline. This code was ported from [Damian Hickey's Limits Middleware for Owin](https://github.com/damianh/LimitsMiddleware). 4 | 5 | ## Features: 6 | 7 | - Max bandwidth 8 | - Max concurrent requests 9 | - Connection timeout 10 | - Max query string 11 | - Max request content length 12 | - Max url length 13 | - Min response delay 14 | 15 | 16 | ## Usage: 17 | 18 | Configuration values can be supplied as constants or with a delegate for runtime values. 19 | 20 | ```csharp 21 | public class Startup 22 | { 23 | public void Configure(IApplicationBuilder app) 24 | { 25 | // static settings 26 | app 27 | .MaxBandwidth(10000) //bps 28 | .MaxConcurrentRequests(10) 29 | .ConnectionTimeout(TimeSpan.FromSeconds(10)) 30 | .MaxQueryStringLength(15) //Unescaped QueryString 31 | .MaxRequestContentLength(15) 32 | .MaxUrlLength(20) 33 | .MinResponseDelay(200ms) 34 | .Use(..); 35 | 36 | // dynamic settings 37 | app 38 | .MaxBandwidth(() => 10000) //bps 39 | .MaxConcurrentRequests(() => 10) 40 | .ConnectionTimeout(() => TimeSpan.FromSeconds(10)) 41 | .MaxQueryStringLength(() => 15) 42 | .MaxRequestContentLength(() => 15) 43 | .MaxUrlLength(() => 20) 44 | .Use(..); 45 | } 46 | } 47 | } 48 | ``` 49 | 50 | ## Thanks 51 | * [Funnel](https://thenounproject.com/term/funnel/515072/) icon by [Arthur Shlain](https://thenounproject.com/ArtZ91/) from [The Noun Project](https://thenounproject.com) 52 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 3.2.0-alpha-{build} 2 | branches: 3 | only: 4 | - master 5 | pull_requests: 6 | do_not_increment_build_number: true 7 | environment: 8 | COVERALLS_REPO_TOKEN: 9 | secure: baW+hRvE5yKbwNrQtIGSC0X+g1z2DaD5YYc5RiLRqkHaLAlzMqgd8lEV0L1tT+IC 10 | COVER_FILTER: '+[Limits*]* -[*]*.Logging.* -[*Tests]*' 11 | build_script: 12 | - ps: .\build\build.ps1 $env:APPVEYOR_BUILD_VERSION $env:APPVEYOR_REPO_TAG_NAME 13 | test_script: 14 | - ps: | 15 | if ($true) 16 | { 17 | .\build\test.ps1 18 | .\build\cover.ps1 "$env:COVERALLS_REPO_TOKEN" "$env:COVER_FILTER" 19 | } 20 | artifacts: 21 | - path: '**\*.nupkg' 22 | deploy: 23 | - provider: NuGet 24 | api_key: 25 | secure: Uft/AgWL0ObDUb6hWLhsftRR1sNhLa5vONUcVa/2KjVAYZApxZD6ckJ+ABFQs3bB 26 | skip_symbols: true 27 | artifact: /.*\.nupkg/ 28 | on: 29 | branch: master # release from master branch only 30 | appveyor_repo_tag: true # deploy on tag push only 31 | -------------------------------------------------------------------------------- /build/build.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string]$BuildVersionNumber=$(throw "-BuildVersionNumber is required."), 3 | [string]$TagVersionNumber 4 | ) 5 | 6 | # set version number in package.json 7 | Get-ChildItem -Path $PSScriptRoot\..\src -Filter project.json -Recurse | ForEach-Object{ 8 | $ProjectJsonPath = $_.FullName 9 | if ($TagVersionNumber){ 10 | (gc -Path $ProjectJsonPath) ` 11 | -replace "(?<=`"version`":\s`")[.\w-]*(?=`",)", "$TagVersionNumber" | 12 | sc -Path $ProjectJsonPath -Encoding UTF8 13 | } 14 | else{ 15 | (gc -Path $ProjectJsonPath) ` 16 | -replace "(?<=`"version`":\s`")[.\w-]*(?=`",)", "$BuildVersionNumber" | 17 | sc -Path $ProjectJsonPath -Encoding UTF8 18 | } 19 | } 20 | 21 | # run restore on all project.json files in the src folder including 2>1 to redirect stderr to stdout for badly behaved tools 22 | Get-ChildItem -Path $PSScriptRoot\..\src -Filter project.json -Recurse | ForEach-Object { & dotnet restore $_.FullName 2>1 } 23 | 24 | # run pack on all project.json files in the src folder including 2>1 to redirect stderr to stdout for badly behaved tools 25 | Get-ChildItem -Path $PSScriptRoot\..\src -Filter project.json -Recurse | ForEach-Object { & dotnet pack $_.FullName -c Release 2>1 } 26 | -------------------------------------------------------------------------------- /build/cover.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string]$CoverallsRepoToken=$(throw "-CoverallsRepoToken is required."), 3 | [string]$CoverFilter=$(throw "-CoverFilter is required.") 4 | ) 5 | 6 | Write-Output "Starting code coverage with filter: $CoverFilter" 7 | 8 | $alwaysFilter = "-[xunit*]* -[Microsoft*]* -[dotnet*]* -[NuGet*]* -[Newtonsoft*]* -[csc]* -[Anonymously*]*" 9 | $filter = "$CoverFilter $alwaysFilter" 10 | 11 | $packagesPath = $env:USERPROFILE + "\.nuget\packages" 12 | $opencoverPath = $packagesPath + "\OpenCover\4.6.519\tools\OpenCover.Console.exe" 13 | $coverallsPath = $packagesPath + "\coveralls.io\1.3.4\tools\coveralls.net.exe" 14 | $tempPath = "c:\codecoverage" 15 | $tempCoveragePath = $tempPath + "\coverage\" 16 | $tempCoverageFileName = $tempCoveragePath + "coverage.xml" 17 | 18 | # create temp path 19 | if (-not (test-path $tempPath) ) { 20 | mkdir $tempPath | Out-Null 21 | } 22 | 23 | # run opencover 24 | Get-ChildItem -Path $PSScriptRoot\..\test -Filter project.json -Recurse | ForEach-Object { 25 | $path = "$tempPath\$($_.Directory.BaseName)" 26 | if (-not (test-path $path) ) { 27 | mkdir $path | Out-Null 28 | } 29 | 30 | $tempBinPath = $path + "\bin\" 31 | $targetArgs = "`"test -o $tempBinPath $($_.FullName)`"" 32 | 33 | if (-not (test-path $tempBinPath) ) { 34 | mkdir $tempBinPath | Out-Null 35 | } 36 | 37 | if (-not (test-path $tempCoveragePath) ) { 38 | mkdir $tempCoveragePath | Out-Null 39 | } 40 | 41 | & $opencoverPath ` 42 | -register:user ` 43 | -target:"dotnet.exe" ` 44 | -targetargs:$targetArgs ` 45 | -searchdirs:$tempBinPath ` 46 | -output:$tempCoverageFileName ` 47 | -mergebyhash ` 48 | -mergeoutput ` 49 | -skipautoprops ` 50 | -returntargetcode ` 51 | -filter:$filter ` 52 | -hideskipped:Filter ` 53 | -oldstyle 54 | } 55 | 56 | # upload to coveralls.io 57 | Write-Output "Sending code coverage results to coveralls.io" 58 | 59 | & $coverallsPath ` 60 | --opencover $tempCoverageFileName ` 61 | --full-sources ` 62 | --repo-token $CoverallsRepoToken 63 | 64 | 7z a codecoverage.zip $tempCoverageFileName 65 | Push-AppveyorArtifact codecoverage.zip 66 | 67 | 68 | -------------------------------------------------------------------------------- /build/test.ps1: -------------------------------------------------------------------------------- 1 | # run restore on all project.json files in the src folder including 2>1 to redirect stderr to stdout for badly behaved tools 2 | Get-ChildItem -Path $PSScriptRoot\..\test -Filter project.json -Recurse | ForEach-Object { & dotnet restore $_.FullName 2>&1 } 3 | 4 | # run tests 5 | Get-ChildItem -Path $PSScriptRoot\..\test -Filter project.json -Recurse | ForEach-Object { & dotnet test -c Release $_.FullName 2>&1 } -------------------------------------------------------------------------------- /demo/LimitsMiddleware.AspNetCore.Demo/LimitsMiddleware.Demo.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | 962b06a8-4d78-4438-878d-34a0f86f2a5c 11 | LimitsMiddleware.Demo 12 | .\obj 13 | .\bin\ 14 | v4.5.2 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /demo/LimitsMiddleware.AspNetCore.Demo/Program.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | namespace LimitsMiddleware.Demo 6 | { 7 | public class Program 8 | { 9 | public static void Main() 10 | { 11 | var config = new ConfigurationBuilder() 12 | .AddJsonFile("appsettings.json") 13 | .SetBasePath(Directory.GetCurrentDirectory()) 14 | .Build(); 15 | 16 | var host = new WebHostBuilder() 17 | .UseKestrel() 18 | .UseConfiguration(config) 19 | .UseContentRoot(Directory.GetCurrentDirectory()) 20 | .UseStartup() 21 | .Build(); 22 | 23 | host.Run(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo/LimitsMiddleware.AspNetCore.Demo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:3749/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "LimitsMiddleware.Demo": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /demo/LimitsMiddleware.AspNetCore.Demo/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using LimitsMiddleware.Extensions; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.WebUtilities; 6 | using Microsoft.Extensions.Primitives; 7 | 8 | namespace LimitsMiddleware.Demo 9 | { 10 | public class Startup 11 | { 12 | public void Configure(IApplicationBuilder app) 13 | { 14 | app 15 | .MaxUrlLength(100) 16 | .MaxQueryStringLength(80) 17 | .MaxConcurrentRequests(4) 18 | 19 | .MinResponseDelay(context => 20 | { 21 | var queryParams = QueryHelpers.ParseQuery(context.Uri.Query); 22 | StringValues minResponseDelayParamValues; 23 | string minResponseDelayParam = queryParams.TryGetValue("minresponsedelay", out minResponseDelayParamValues) 24 | ? minResponseDelayParamValues.First() 25 | : null; 26 | int minResponseDelay; 27 | return int.TryParse(minResponseDelayParam, out minResponseDelay) 28 | ? TimeSpan.FromSeconds(minResponseDelay) 29 | : TimeSpan.Zero; 30 | }) 31 | 32 | .MaxBandwidthPerRequest(context => 33 | { 34 | var queryParams = QueryHelpers.ParseQuery(context.Uri.Query); 35 | StringValues maxBandWidthParamValues; 36 | string maxBandwidthParam = queryParams.TryGetValue("maxbandwidthperrequest", out maxBandWidthParamValues) 37 | ? maxBandWidthParamValues.First() 38 | : null; 39 | int maxBandwidth; 40 | return int.TryParse(maxBandwidthParam, out maxBandwidth) 41 | ? maxBandwidth 42 | : -1; 43 | }) 44 | 45 | .MaxBandwidthGlobal(10 * 1024 * 1024); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /demo/LimitsMiddleware.AspNetCore.Demo/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "Microsoft.NETCore.App": { 4 | "version": "1.0.0", 5 | "type": "platform" 6 | }, 7 | "Microsoft.AspNetCore.Diagnostics": "1.0.0", 8 | "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", 9 | "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", 10 | "Microsoft.Extensions.Logging.Console": "1.0.0", 11 | "Microsoft.Extensions.Configuration.Json": "1.1.0-alpha1-21868", 12 | "LimitsMiddleware.AspNetCore": { "target": "project" } 13 | }, 14 | 15 | "tools": { 16 | "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" 17 | }, 18 | 19 | "frameworks": { 20 | "netcoreapp1.0": { } 21 | }, 22 | 23 | "buildOptions": { 24 | "define": [ "LIBLOG_PORTABLE" ], 25 | "emitEntryPoint": true, 26 | "preserveCompilationContext": true 27 | }, 28 | 29 | "runtimeOptions": { 30 | "configProperties": { 31 | "System.GC.Server": true 32 | } 33 | }, 34 | 35 | "publishOptions": { 36 | "include": [ 37 | "wwwroot", 38 | "web.config" 39 | ] 40 | }, 41 | 42 | "scripts": { 43 | "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /demo/LimitsMiddleware.AspNetCore.Demo/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src", "test" ], 3 | "sdk": { "version": "1.0.0-preview2-003131" } 4 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/ContentLengthExceededException.cs: -------------------------------------------------------------------------------- 1 | // https://github.com/robertmircea/RateLimiters 2 | 3 | namespace LimitsMiddleware 4 | { 5 | using System; 6 | 7 | internal class ContentLengthExceededException : Exception 8 | { 9 | public ContentLengthExceededException(string message) : base(message) 10 | {} 11 | } 12 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/ContentLengthLimitingStream.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware 2 | { 3 | using System.IO; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | // Template from http://forums.asp.net/t/1966232.aspx?Web+API+OWIN+Host+Limit+Request+Size 8 | 9 | internal class ContentLengthLimitingStream : Stream 10 | { 11 | private readonly Stream _innerStream; 12 | private readonly long _maxRequestSizeInBytes; 13 | private long _totalBytesReadCount; 14 | 15 | public ContentLengthLimitingStream(Stream innerStream, long maxReceivedMessageSize) 16 | { 17 | _innerStream = innerStream; 18 | _maxRequestSizeInBytes = maxReceivedMessageSize; 19 | } 20 | 21 | protected Stream InnerStream => _innerStream; 22 | 23 | public override bool CanRead => _innerStream.CanRead; 24 | 25 | public override bool CanSeek => _innerStream.CanSeek; 26 | 27 | public override bool CanWrite => _innerStream.CanWrite; 28 | 29 | public override long Length => _innerStream.Length; 30 | 31 | public override long Position 32 | { 33 | get { return _innerStream.Position; } 34 | set { _innerStream.Position = value; } 35 | } 36 | 37 | public override int ReadTimeout 38 | { 39 | get { return _innerStream.ReadTimeout; } 40 | set { _innerStream.ReadTimeout = value; } 41 | } 42 | 43 | public override bool CanTimeout => _innerStream.CanTimeout; 44 | 45 | public override int WriteTimeout 46 | { 47 | get { return _innerStream.WriteTimeout; } 48 | set { _innerStream.WriteTimeout = value; } 49 | } 50 | 51 | protected override void Dispose(bool disposing) 52 | { 53 | if (disposing) 54 | { 55 | _innerStream.Dispose(); 56 | } 57 | base.Dispose(disposing); 58 | } 59 | 60 | public override long Seek(long offset, SeekOrigin origin) 61 | { 62 | return _innerStream.Seek(offset, origin); 63 | } 64 | 65 | public override int Read(byte[] buffer, int offset, int count) 66 | { 67 | int currentNumberOfBytesRead = _innerStream.Read(buffer, offset, count); 68 | 69 | ValidateRequestSize(currentNumberOfBytesRead); 70 | 71 | return currentNumberOfBytesRead; 72 | } 73 | 74 | public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 75 | { 76 | int currentNumberOfBytesRead = await _innerStream.ReadAsync(buffer, offset, count, cancellationToken); 77 | 78 | ValidateRequestSize(currentNumberOfBytesRead); 79 | 80 | return currentNumberOfBytesRead; 81 | } 82 | 83 | public override int ReadByte() 84 | { 85 | int currentNumberOfBytesRead = _innerStream.ReadByte(); 86 | 87 | ValidateRequestSize(currentNumberOfBytesRead); 88 | 89 | return currentNumberOfBytesRead; 90 | } 91 | 92 | public override void Flush() 93 | { 94 | _innerStream.Flush(); 95 | } 96 | 97 | public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) 98 | { 99 | return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); 100 | } 101 | 102 | public override Task FlushAsync(CancellationToken cancellationToken) 103 | { 104 | return _innerStream.FlushAsync(cancellationToken); 105 | } 106 | 107 | public override void SetLength(long value) 108 | { 109 | _innerStream.SetLength(value); 110 | } 111 | 112 | public override void Write(byte[] buffer, int offset, int count) 113 | { 114 | _innerStream.Write(buffer, offset, count); 115 | } 116 | 117 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 118 | { 119 | return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); 120 | } 121 | 122 | public override void WriteByte(byte value) 123 | { 124 | _innerStream.WriteByte(value); 125 | } 126 | 127 | private void ValidateRequestSize(int currentNumberOfBytesRead) 128 | { 129 | _totalBytesReadCount += currentNumberOfBytesRead; 130 | 131 | if (_totalBytesReadCount > 0 && _maxRequestSizeInBytes == 0) 132 | { 133 | throw new ContentLengthRequiredException(); 134 | } 135 | 136 | if (_totalBytesReadCount > _maxRequestSizeInBytes) 137 | { 138 | throw new ContentLengthExceededException($"Request size exceeds the allowed maximum size of {_maxRequestSizeInBytes} bytes"); 139 | } 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/ContentLengthRequiredException.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware 2 | { 3 | using System; 4 | 5 | internal class ContentLengthRequiredException : Exception 6 | {} 7 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Extensions/ApplicationBuilderExtensions.ConnectionTimeout.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace LimitsMiddleware.Extensions 6 | { 7 | public static partial class ApplicationBuilderExtensions 8 | { 9 | /// 10 | /// Timeouts the connection if there hasn't been an read activity on the request body stream or any 11 | /// write activity on the response body stream. 12 | /// 13 | /// The IApplicationBuilder instance. 14 | /// The timeout. 15 | /// (Optional) The name of the logger log messages are written to. 16 | /// The IApplicationBuilder instance. 17 | public static IApplicationBuilder ConnectionTimeout(this IApplicationBuilder app, TimeSpan timeout, string loggerName = null) 18 | { 19 | if (app == null) 20 | { 21 | throw new ArgumentNullException(nameof(app)); 22 | } 23 | 24 | app.Use(Limits.ConnectionTimeout(timeout, loggerName)); 25 | 26 | return app; 27 | } 28 | 29 | /// 30 | /// Timeouts the connection if there hasn't been an read activity on the request body stream or any 31 | /// write activity on the response body stream. 32 | /// 33 | /// The IApplicationBuilder instance. 34 | /// 35 | /// A delegate to retrieve the timeout timespan. Allows you 36 | /// to supply different values at runtime. 37 | /// 38 | /// (Optional) The name of the logger log messages are written to. 39 | /// The IApplicationBuilder instance. 40 | public static IApplicationBuilder ConnectionTimeout(this IApplicationBuilder app, Func getTimeout, string loggerName = null) 41 | { 42 | if (app == null) 43 | { 44 | throw new ArgumentNullException(nameof(app)); 45 | } 46 | if (getTimeout == null) 47 | { 48 | throw new ArgumentNullException(nameof(getTimeout)); 49 | } 50 | 51 | app.Use(Limits.ConnectionTimeout(getTimeout, loggerName)); 52 | 53 | return app; 54 | } 55 | 56 | 57 | /// 58 | /// Timeouts the connection if there hasn't been an read activity on the request body stream or any 59 | /// write activity on the response body stream. 60 | /// 61 | /// The IApplicationBuilder instance. 62 | /// 63 | /// A delegate to retrieve the timeout timespan. Allows you 64 | /// to supply different values at runtime. 65 | /// 66 | /// (Optional) The name of the logger log messages are written to. 67 | /// A middleware delegate. 68 | /// getTimeout 69 | public static IApplicationBuilder ConnectionTimeout(this IApplicationBuilder app, Func getTimeout, string loggerName = null) 70 | { 71 | if (app == null) 72 | { 73 | throw new ArgumentNullException(nameof(app)); 74 | } 75 | if (getTimeout == null) 76 | { 77 | throw new ArgumentNullException(nameof(getTimeout)); 78 | } 79 | 80 | app.Use(async (context, next) => 81 | { 82 | Limits.ConnectionTimeout(getTimeout, loggerName); 83 | await next.Invoke(); 84 | }); 85 | Func connectionTimeout = Limits.ConnectionTimeout(getTimeout, loggerName); 86 | app.Use(connectionTimeout); 87 | 88 | return app; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Extensions/ApplicationBuilderExtensions.MaxBandwidthGlobal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | 4 | namespace LimitsMiddleware.Extensions 5 | { 6 | public static partial class ApplicationBuilderExtensions 7 | { 8 | /// 9 | /// Limits the bandwith used globally by the subsequent stages in the aspnetcore pipeline. 10 | /// 11 | /// The IApplicationBuilder instance. 12 | /// 13 | /// The maximum number of bytes per second to be transferred. Use 0 or a negative 14 | /// number to specify infinite bandwidth. 15 | /// 16 | /// (Optional) The name of the logger log messages are written to. 17 | /// The IApplicationBuilder instance. 18 | public static IApplicationBuilder MaxBandwidthGlobal(this IApplicationBuilder app, int maxBytesPerSecond, 19 | string loggerName = null) 20 | { 21 | if (app == null) 22 | { 23 | throw new ArgumentNullException(nameof(app)); 24 | } 25 | 26 | return MaxBandwidthGlobal(app, () => maxBytesPerSecond, loggerName); 27 | } 28 | 29 | /// 30 | /// Limits the bandwith used globally by the subsequent stages in the aspnetcore pipeline. 31 | /// 32 | /// The IApplicationBuilder instance. 33 | /// 34 | /// A delegate to retrieve the maximum number of bytes per second to be transferred. 35 | /// Allows you to supply different values at runtime. Use 0 or a negative number to specify infinite bandwidth. 36 | /// 37 | /// (Optional) The name of the logger log messages are written to. 38 | /// The app instance. 39 | public static IApplicationBuilder MaxBandwidthGlobal(this IApplicationBuilder app, Func getMaxBytesPerSecond, string loggerName = null) 40 | { 41 | if (app == null) 42 | { 43 | throw new ArgumentNullException(nameof(app)); 44 | } 45 | if (getMaxBytesPerSecond == null) 46 | { 47 | throw new ArgumentNullException(nameof(getMaxBytesPerSecond)); 48 | } 49 | 50 | app.Use(Limits.MaxBandwidthGlobal(getMaxBytesPerSecond, loggerName)); 51 | return app; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Extensions/ApplicationBuilderExtensions.MaxBandwidthPerRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | 4 | namespace LimitsMiddleware.Extensions 5 | { 6 | public static partial class ApplicationBuilderExtensions 7 | { 8 | /// 9 | /// Limits the bandwith used by the subsequent stages in the aspnetcore pipeline. 10 | /// 11 | /// The IApplicationBuilder instance. 12 | /// 13 | /// The maximum number of bytes per second to be transferred. Use 0 or a negative 14 | /// number to specify infinite bandwidth. 15 | /// 16 | /// The IApplicationBuilder instance. 17 | public static IApplicationBuilder MaxBandwidthPerRequest(this IApplicationBuilder app, int maxBytesPerSecond) 18 | { 19 | if (app == null) 20 | { 21 | throw new ArgumentNullException(nameof(app)); 22 | } 23 | 24 | return MaxBandwidthPerRequest(app, () => maxBytesPerSecond); 25 | } 26 | 27 | /// 28 | /// Limits the bandwith used by the subsequent stages in the aspnetcore pipeline. 29 | /// 30 | /// The IApplicationBuilder instance. 31 | /// 32 | /// A delegate to retrieve the maximum number of bytes per second to be transferred. 33 | /// Allows you to supply different values at runtime. Use 0 or a negative number to specify infinite bandwidth. 34 | /// 35 | /// The app instance. 36 | public static IApplicationBuilder MaxBandwidthPerRequest(this IApplicationBuilder app, Func getMaxBytesPerSecond) 37 | { 38 | if (app == null) 39 | { 40 | throw new ArgumentNullException(nameof(app)); 41 | } 42 | if (getMaxBytesPerSecond == null) 43 | { 44 | throw new ArgumentNullException(nameof(getMaxBytesPerSecond)); 45 | } 46 | 47 | app.Use(Limits.MaxBandwidthPerRequest(getMaxBytesPerSecond)); 48 | return app; 49 | } 50 | 51 | /// 52 | /// Limits the bandwith used by the subsequent stages in the aspnetcore pipeline. 53 | /// 54 | /// 55 | /// A delegate to retrieve the maximum number of bytes per second to be transferred. 56 | /// Allows you to supply different values at runtime. Use 0 or a negative number to specify infinite bandwidth. 57 | /// 58 | /// A middleware delegate. 59 | /// The IApplicationBuilder instance. 60 | /// app 61 | /// getMaxBytesPerSecond 62 | public static IApplicationBuilder MaxBandwidthPerRequest(this IApplicationBuilder app, Func getMaxBytesPerSecond) 63 | { 64 | if (app == null) 65 | { 66 | throw new ArgumentNullException(nameof(app)); 67 | } 68 | if (getMaxBytesPerSecond == null) 69 | { 70 | throw new ArgumentNullException(nameof(getMaxBytesPerSecond)); 71 | } 72 | 73 | app.Use(Limits.MaxBandwidthPerRequest(getMaxBytesPerSecond)); 74 | return app; 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Extensions/ApplicationBuilderExtensions.MaxConcurrentRequests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | 4 | namespace LimitsMiddleware.Extensions 5 | { 6 | public static partial class ApplicationBuilderExtensions 7 | { 8 | /// 9 | /// Limits the number of concurrent requests that can be handled used by the subsequent stages in the aspnetcore pipeline. 10 | /// 11 | /// The IApplicationBuilder instance. 12 | /// 13 | /// The maximum number of concurrent requests. Use 0 or a negative 14 | /// number to specify unlimited number of concurrent requests. 15 | /// 16 | /// (Optional) The name of the logger log messages are written to. 17 | /// The IApplicationBuilder instance. 18 | public static IApplicationBuilder MaxConcurrentRequests(this IApplicationBuilder app, int maxConcurrentRequests, string loggerName = null) 19 | { 20 | if (app == null) 21 | { 22 | throw new ArgumentNullException(nameof(app)); 23 | } 24 | 25 | return MaxConcurrentRequests(app, () => maxConcurrentRequests, loggerName); 26 | } 27 | 28 | /// 29 | /// Limits the number of concurrent requests that can be handled used by the subsequent stages in the aspnetcore pipeline. 30 | /// 31 | /// The IApplicationBuilder instance. 32 | /// 33 | /// A delegate to retrieve the maximum number of concurrent requests. Allows you 34 | /// to supply different values at runtime. Use 0 or a negative number to specify unlimited number of concurrent 35 | /// requests. 36 | /// 37 | /// (Optional) The name of the logger log messages are written to. 38 | /// The IApplicationBuilder instance. 39 | public static IApplicationBuilder MaxConcurrentRequests(this IApplicationBuilder app, Func getMaxConcurrentRequests, string loggerName = null) 40 | { 41 | if (app == null) 42 | { 43 | throw new ArgumentNullException(nameof(app)); 44 | } 45 | if (getMaxConcurrentRequests == null) 46 | { 47 | throw new ArgumentNullException(nameof(getMaxConcurrentRequests)); 48 | } 49 | 50 | app.Use(Limits.MaxConcurrentRequests(getMaxConcurrentRequests, loggerName)); 51 | 52 | return app; 53 | } 54 | 55 | /// 56 | /// Limits the number of concurrent requests that can be handled used by the subsequent stages in the aspnetcore pipeline. 57 | /// 58 | /// The IApplicationBuilder instance. 59 | /// 60 | /// A delegate to retrieve the maximum number of concurrent requests. Allows you 61 | /// to supply different values at runtime. Use 0 or a negative number to specify unlimited number of concurrent 62 | /// requests. 63 | /// 64 | /// (Optional) The name of the logger log messages are written to. 65 | /// The IApplicationBuilder instance. 66 | public static IApplicationBuilder MaxConcurrentRequests(this IApplicationBuilder app, Func getMaxConcurrentRequests, string loggerName = null) 67 | { 68 | if (app == null) 69 | { 70 | throw new ArgumentNullException(nameof(app)); 71 | } 72 | if (getMaxConcurrentRequests == null) 73 | { 74 | throw new ArgumentNullException(nameof(getMaxConcurrentRequests)); 75 | } 76 | 77 | app.Use(Limits.MaxConcurrentRequests(getMaxConcurrentRequests, loggerName)); 78 | 79 | return app; 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Extensions/ApplicationBuilderExtensions.MaxQueryStringLength.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | 4 | namespace LimitsMiddleware.Extensions 5 | { 6 | public static partial class ApplicationBuilderExtensions 7 | { 8 | /// 9 | /// Limits the length of the query string. 10 | /// 11 | /// The IApplicationBuilder instance. 12 | /// Maximum length of the query string. 13 | /// (Optional) The name of the logger log messages are written to. 14 | /// The IApplicationBuilder instance. 15 | public static IApplicationBuilder MaxQueryStringLength(this IApplicationBuilder app, int maxQueryStringLength, 16 | string loggerName = null) 17 | { 18 | if (app == null) 19 | { 20 | throw new ArgumentNullException(nameof(app)); 21 | } 22 | 23 | return MaxQueryStringLength(app, () => maxQueryStringLength, loggerName); 24 | } 25 | 26 | /// 27 | /// Limits the length of the query string. 28 | /// 29 | /// The IApplicationBuilder instance. 30 | /// A delegate to get the maximum query string length. 31 | /// (Optional) The name of the logger log messages are written to. 32 | /// The IApplicationBuilder instance. 33 | public static IApplicationBuilder MaxQueryStringLength(this IApplicationBuilder app, Func getMaxQueryStringLength, 34 | string loggerName = null) 35 | { 36 | if (app == null) 37 | { 38 | throw new ArgumentNullException(nameof(app)); 39 | } 40 | if (getMaxQueryStringLength == null) 41 | { 42 | throw new ArgumentNullException(nameof(getMaxQueryStringLength)); 43 | } 44 | 45 | app.Use(Limits.MaxQueryStringLength(getMaxQueryStringLength, loggerName)); 46 | 47 | return app; 48 | } 49 | 50 | 51 | /// 52 | /// Limits the length of the query string. 53 | /// 54 | /// The IApplicationBuilder instance. 55 | /// A delegate to get the maximum query string length. 56 | /// (Optional) The name of the logger log messages are written to. 57 | /// The IApplicationBuilder instance. 58 | public static IApplicationBuilder MaxQueryStringLength(this IApplicationBuilder app, 59 | Func getMaxQueryStringLength, string loggerName = null) 60 | { 61 | if (app == null) 62 | { 63 | throw new ArgumentNullException(nameof(app)); 64 | } 65 | if (getMaxQueryStringLength == null) 66 | { 67 | throw new ArgumentNullException(nameof(getMaxQueryStringLength)); 68 | } 69 | 70 | app.Use(Limits.MaxQueryStringLength(getMaxQueryStringLength, loggerName)); 71 | 72 | return app; 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Extensions/ApplicationBuilderExtensions.MaxRequestContentLength.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | 4 | namespace LimitsMiddleware.Extensions 5 | { 6 | public static partial class ApplicationBuilderExtensions 7 | { 8 | /// 9 | /// Limits the length of the request content. 10 | /// 11 | /// The IApplicationBuilder instance. 12 | /// Maximum length of the content. 13 | /// (Optional) The name of the logger log messages are written to. 14 | /// The IApplicationBuilder instance. 15 | public static IApplicationBuilder MaxRequestContentLength(this IApplicationBuilder app, int maxContentLength, string loggerName = null) 16 | { 17 | if (app == null) 18 | { 19 | throw new ArgumentNullException(nameof(app)); 20 | } 21 | 22 | return MaxRequestContentLength(app, () => maxContentLength, loggerName); 23 | } 24 | 25 | /// 26 | /// Limits the length of the request content. 27 | /// 28 | /// The IApplicationBuilder instance. 29 | /// A delegate to get the maximum content length. 30 | /// (Optional) The name of the logger log messages are written to. 31 | /// The IApplicationBuilder instance. 32 | public static IApplicationBuilder MaxRequestContentLength(this IApplicationBuilder app, Func getMaxContentLength, string loggerName = null) 33 | { 34 | if (app == null) 35 | { 36 | throw new ArgumentNullException(nameof(app)); 37 | } 38 | if (getMaxContentLength == null) 39 | { 40 | throw new ArgumentNullException(nameof(getMaxContentLength)); 41 | } 42 | 43 | app.Use(Limits.MaxRequestContentLength(getMaxContentLength, loggerName)); 44 | 45 | return app; 46 | } 47 | 48 | /// 49 | /// Limits the length of the request content. 50 | /// 51 | /// The IApplicationBuilder instance. 52 | /// A delegate to get the maximum content length. 53 | /// (Optional) The name of the logger log messages are written to. 54 | /// The IApplicationBuilder instance. 55 | public static IApplicationBuilder MaxRequestContentLength(this IApplicationBuilder app, Func getMaxContentLength, string loggerName = null) 56 | { 57 | if (app == null) 58 | { 59 | throw new ArgumentNullException(nameof(app)); 60 | } 61 | if (getMaxContentLength == null) 62 | { 63 | throw new ArgumentNullException(nameof(getMaxContentLength)); 64 | } 65 | 66 | app.Use(Limits.MaxRequestContentLength(getMaxContentLength, loggerName)); 67 | 68 | return app; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Extensions/ApplicationBuilderExtensions.MaxUrlLength.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | 4 | namespace LimitsMiddleware.Extensions 5 | { 6 | public static partial class ApplicationBuilderExtensions 7 | { 8 | /// 9 | /// Limits the length of the URL. 10 | /// 11 | /// The IApplicationBuilder instance. 12 | /// Maximum length of the URL. 13 | /// (Optional) The name of the logger log messages are written to. 14 | /// The IApplicationBuilder instance. 15 | public static IApplicationBuilder MaxUrlLength(this IApplicationBuilder app, int maxUrlLength, string loggerName = null) 16 | { 17 | if (app == null) 18 | { 19 | throw new ArgumentNullException(nameof(app)); 20 | } 21 | 22 | return MaxUrlLength(app, () => maxUrlLength, loggerName); 23 | } 24 | 25 | /// 26 | /// Limits the length of the URL. 27 | /// 28 | /// The IApplicationBuilder instance. 29 | /// A delegate to get the maximum URL length. 30 | /// (Optional) The name of the logger log messages are written to. 31 | /// The IApplicationBuilder instance. 32 | public static IApplicationBuilder MaxUrlLength(this IApplicationBuilder app, Func getMaxUrlLength, string loggerName = null) 33 | { 34 | if (app == null) 35 | { 36 | throw new ArgumentNullException(nameof(app)); 37 | } 38 | if (getMaxUrlLength == null) 39 | { 40 | throw new ArgumentNullException(nameof(getMaxUrlLength)); 41 | } 42 | 43 | app.Use(Limits.MaxUrlLength(getMaxUrlLength, loggerName)); 44 | return app; 45 | } 46 | 47 | /// 48 | /// Limits the length of the URL. 49 | /// 50 | /// The IApplicationBuilder instance. 51 | /// A delegate to get the maximum URL length. 52 | /// (Optional) The name of the logger log messages are written to. 53 | /// The IApplicationBuilder instance. 54 | public static IApplicationBuilder MaxUrlLength(this IApplicationBuilder app, Func getMaxUrlLength, string loggerName = null) 55 | { 56 | if (app == null) 57 | { 58 | throw new ArgumentNullException(nameof(app)); 59 | } 60 | if (getMaxUrlLength == null) 61 | { 62 | throw new ArgumentNullException(nameof(getMaxUrlLength)); 63 | } 64 | 65 | app.Use(Limits.MaxUrlLength(getMaxUrlLength, loggerName)); 66 | return app; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Extensions/ApplicationBuilderExtensions.MinResponseDelay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | 4 | namespace LimitsMiddleware.Extensions 5 | { 6 | public static partial class ApplicationBuilderExtensions 7 | { 8 | /// 9 | /// Sets a minimum delay in miliseconds before sending the response. 10 | /// 11 | /// The IApplicationBuilder instance. 12 | /// 13 | /// The minimum delay to wait before sending the response. 14 | /// 15 | /// (Optional) The name of the logger log messages are written to. 16 | /// The IApplicationBuilder instance. 17 | public static IApplicationBuilder MinResponseDelay(this IApplicationBuilder app, int minDelay, string loggerName = null) 18 | { 19 | if (app == null) 20 | { 21 | throw new ArgumentNullException(nameof(app)); 22 | } 23 | 24 | return MinResponseDelay(app, () => minDelay, loggerName); 25 | } 26 | 27 | /// 28 | /// Sets a minimum delay before sending the response. 29 | /// 30 | /// The IApplicationBuilder instance. 31 | /// 32 | /// A delegate to retrieve the maximum number of bytes per second to be transferred. 33 | /// Allows you to supply different values at runtime. Use 0 or a negative number to specify infinite bandwidth. 34 | /// 35 | /// (Optional) The name of the logger log messages are written to. 36 | /// The IApplicationBuilder instance. 37 | public static IApplicationBuilder MinResponseDelay(this IApplicationBuilder app, Func getMinDelay, string loggerName = null) 38 | { 39 | if (app == null) 40 | { 41 | throw new ArgumentNullException(nameof(app)); 42 | } 43 | if (getMinDelay == null) 44 | { 45 | throw new ArgumentNullException(nameof(getMinDelay)); 46 | } 47 | 48 | app.Use(Limits.MinResponseDelay(getMinDelay, loggerName)); 49 | return app; 50 | } 51 | 52 | /// 53 | /// Sets a minimum delay before sending the response. 54 | /// 55 | /// The IApplicationBuilder instance. 56 | /// 57 | /// A delegate to retrieve the minimum delay before calling the next stage in the pipeline. Note: 58 | /// the delegate should return quickly. 59 | /// 60 | /// (Optional) The name of the logger log messages are written to. 61 | /// The IApplicationBuilder instance. 62 | public static IApplicationBuilder MinResponseDelay(this IApplicationBuilder app, Func getMinDelay, string loggerName = null) 63 | { 64 | if (app == null) 65 | { 66 | throw new ArgumentNullException(nameof(app)); 67 | } 68 | if (getMinDelay == null) 69 | { 70 | throw new ArgumentNullException(nameof(getMinDelay)); 71 | } 72 | 73 | app.Use(Limits.MinResponseDelay(getMinDelay, loggerName)); 74 | return app; 75 | } 76 | 77 | /// 78 | /// Sets a minimum delay before sending the response. 79 | /// 80 | /// The IApplicationBuilder instance. 81 | /// 82 | /// A delegate to retrieve the minimum delay before calling the next stage in the pipeline. Note: 83 | /// the delegate should return quickly. 84 | /// 85 | /// (Optional) The name of the logger log messages are written to. 86 | /// The IApplicationBuilder instance. 87 | public static IApplicationBuilder MinResponseDelay(this IApplicationBuilder app, Func getMinDelay, string loggerName = null) 88 | { 89 | if (app == null) 90 | { 91 | throw new ArgumentNullException(nameof(app)); 92 | } 93 | if (getMinDelay == null) 94 | { 95 | throw new ArgumentNullException(nameof(getMinDelay)); 96 | } 97 | 98 | app.Use(Limits.MinResponseDelay(getMinDelay, loggerName)); 99 | return app; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/InterlockedBoolean.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2013 Hans Wolff 3 | // 4 | // Source: https://gist.github.com/hanswolff/7926751 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | 20 | namespace LimitsMiddleware 21 | { 22 | using System.Threading; 23 | 24 | /// 25 | /// Interlocked support for boolean values 26 | /// 27 | internal class InterlockedBoolean 28 | { 29 | private int _value; 30 | 31 | /// 32 | /// Current value 33 | /// 34 | public bool Value => _value == 1; 35 | 36 | /// 37 | /// Initializes a new instance of 38 | /// 39 | /// initial value 40 | public InterlockedBoolean(bool initialValue = false) 41 | { 42 | _value = initialValue ? 1 : 0; 43 | } 44 | 45 | /// 46 | /// Sets a new value 47 | /// 48 | /// new value 49 | /// the original value before any operation was performed 50 | public bool Set(bool newValue) 51 | { 52 | int oldValue = Interlocked.Exchange(ref _value, newValue ? 1 : 0); 53 | return oldValue == 1; 54 | } 55 | 56 | /// 57 | /// Compares the current value and the comparand for equality and, if they are equal, 58 | /// replaces the current value with the new value in an atomic/thread-safe operation. 59 | /// 60 | /// new value 61 | /// value to compare the current value with 62 | /// the original value before any operation was performed 63 | public bool CompareExchange(bool newValue, bool comparand) 64 | { 65 | int oldValue = Interlocked.CompareExchange(ref _value, newValue ? 1 : 0, comparand ? 1 : 0); 66 | return oldValue == 1; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/InterlockedBooleanExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware 2 | { 3 | internal static class InterlockedBooleanExtensions 4 | { 5 | internal static bool EnsureCalledOnce(this InterlockedBoolean interlockedBoolean) 6 | { 7 | return interlockedBoolean.CompareExchange(true, false); 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Limits.ConnectionTimeout.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using LimitsMiddleware.Logging; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace LimitsMiddleware 7 | { 8 | using MidFunc = Func; 9 | 10 | public static partial class Limits 11 | { 12 | /// 13 | /// Timeouts the connection if there hasn't been an read activity on the request body stream or any 14 | /// write activity on the response body stream. 15 | /// 16 | /// (Optional) The name of the logger log messages are written to. 17 | /// The timeout. 18 | /// A middleware delegate. 19 | public static MidFunc ConnectionTimeout(TimeSpan timeout, string loggerName = null) => ConnectionTimeout(() => timeout, loggerName); 20 | 21 | /// 22 | /// Timeouts the connection if there hasn't been an read activity on the request body stream or any 23 | /// write activity on the response body stream. 24 | /// 25 | /// 26 | /// A delegate to retrieve the timeout timespan. Allows you 27 | /// to supply different values at runtime. 28 | /// 29 | /// (Optional) The name of the logger log messages are written to. 30 | /// A middleware delegate. 31 | /// getTimeout 32 | public static MidFunc ConnectionTimeout(Func getTimeout, string loggerName = null) 33 | { 34 | if (getTimeout == null) 35 | { 36 | throw new ArgumentNullException(nameof(getTimeout)); 37 | } 38 | 39 | return ConnectionTimeout(_ => getTimeout(), loggerName); 40 | } 41 | 42 | /// 43 | /// Timeouts the connection if there hasn't been an read activity on the request body stream or any 44 | /// write activity on the response body stream. 45 | /// 46 | /// 47 | /// A delegate to retrieve the timeout timespan. Allows you 48 | /// to supply different values at runtime. 49 | /// 50 | /// (Optional) The name of the logger log messages are written to. 51 | /// A middleware delegate. 52 | /// getTimeout 53 | public static MidFunc ConnectionTimeout(Func getTimeout, string loggerName = null) 54 | { 55 | if (getTimeout == null) 56 | { 57 | throw new ArgumentNullException(nameof(getTimeout)); 58 | } 59 | 60 | loggerName = string.IsNullOrWhiteSpace(loggerName) 61 | ? $"{nameof(LimitsMiddleware)}.{nameof(ConnectionTimeout)}" 62 | : loggerName; 63 | 64 | var logger = LogProvider.GetLogger(loggerName); 65 | 66 | return 67 | next => 68 | context => 69 | { 70 | var limitsRequestContext = new RequestContext(context.Request); 71 | 72 | var requestBodyStream = context.Request.Body ?? Stream.Null; 73 | var responseBodyStream = context.Response.Body; 74 | 75 | var connectionTimeout = getTimeout(limitsRequestContext); 76 | context.Request.Body = new TimeoutStream(requestBodyStream, connectionTimeout, logger); 77 | context.Response.Body = new TimeoutStream(responseBodyStream, connectionTimeout, logger); 78 | return next(context); 79 | }; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Limits.MaxBandwidthGlobal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using LimitsMiddleware.Logging; 4 | using LimitsMiddleware.RateLimiters; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace LimitsMiddleware 8 | { 9 | using MidFunc = Func; 10 | 11 | public static partial class Limits 12 | { 13 | /// 14 | /// Limits the bandwith used by the subsequent stages in the aspnetcore pipeline. 15 | /// 16 | /// 17 | /// The maximum number of bytes per second to be transferred. Use 0 or a negative 18 | /// number to specify infinite bandwidth. 19 | /// 20 | /// (Optional) The name of the logger log messages are written to. 21 | /// A middleware delegate. 22 | public static MidFunc MaxBandwidthGlobal(int maxBytesPerSecond, string loggerName) => MaxBandwidthGlobal(() => maxBytesPerSecond, loggerName); 23 | 24 | /// 25 | /// Limits the bandwith used by the subsequent stages in the aspnetcore pipeline. 26 | /// 27 | /// 28 | /// A delegate to retrieve the maximum number of bytes per second to be transferred. 29 | /// Allows you to supply different values at runtime. Use 0 or a negative number to specify infinite bandwidth. 30 | /// 31 | /// (Optional) The name of the logger log messages are written to. 32 | /// A middleware delegate. 33 | /// getMaxBytesToWrite 34 | public static MidFunc MaxBandwidthGlobal(Func getBytesPerSecond, string loggerName = null) 35 | { 36 | if (getBytesPerSecond == null) 37 | { 38 | throw new ArgumentNullException(nameof(getBytesPerSecond)); 39 | } 40 | 41 | loggerName = string.IsNullOrWhiteSpace(loggerName) 42 | ? $"{nameof(LimitsMiddleware)}.{nameof(MaxBandwidthGlobal)}" 43 | : loggerName; 44 | var logger = LogProvider.GetLogger(loggerName); 45 | 46 | var requestTokenBucket = new FixedTokenBucket(getBytesPerSecond); 47 | var responseTokenBucket = new FixedTokenBucket(getBytesPerSecond); 48 | logger.Debug("Configure streams to be globally limited to."); 49 | 50 | return 51 | next => 52 | async context => 53 | { 54 | // ReSharper disable once ArrangeBraces_using 55 | using (requestTokenBucket.RegisterRequest()) 56 | using (responseTokenBucket.RegisterRequest()) 57 | { 58 | var requestBodyStream = context.Request.Body ?? Stream.Null; 59 | var responseBodyStream = context.Response.Body; 60 | 61 | context.Request.Body = new ThrottledStream(requestBodyStream, requestTokenBucket); 62 | context.Response.Body = new ThrottledStream(responseBodyStream, responseTokenBucket); 63 | 64 | //TODO consider SendFile interception 65 | await next(context).ConfigureAwait(false); 66 | } 67 | }; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Limits.MaxBandwidthPerRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using LimitsMiddleware.RateLimiters; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace LimitsMiddleware 7 | { 8 | using MidFunc = Func; 9 | 10 | public static partial class Limits 11 | { 12 | /// 13 | /// Limits the bandwith used by the subsequent stages in the aspnetcore pipeline. 14 | /// 15 | /// 16 | /// The maximum number of bytes per second to be transferred. Use 0 or a negative 17 | /// number to specify infinite bandwidth. 18 | /// 19 | /// A middleware delegate. 20 | public static MidFunc MaxBandwidthPerRequest(int maxBytesPerSecond) => MaxBandwidthPerRequest(() => maxBytesPerSecond); 21 | 22 | /// 23 | /// Limits the bandwith used by the subsequent stages in the aspnetcore pipeline. 24 | /// 25 | /// 26 | /// A delegate to retrieve the maximum number of bytes per second to be transferred. 27 | /// Allows you to supply different values at runtime. Use 0 or a negative number to specify infinite bandwidth. 28 | /// 29 | /// A middleware delegate. 30 | /// getMaxBytesPerSecond 31 | public static MidFunc MaxBandwidthPerRequest(Func getMaxBytesPerSecond) 32 | { 33 | if (getMaxBytesPerSecond == null) 34 | { 35 | throw new ArgumentNullException(nameof(getMaxBytesPerSecond)); 36 | } 37 | 38 | return MaxBandwidthPerRequest(_ => getMaxBytesPerSecond()); 39 | } 40 | 41 | /// 42 | /// Limits the bandwith used by the subsequent stages in the aspnetcore pipeline. 43 | /// 44 | /// 45 | /// A delegate to retrieve the maximum number of bytes per second to be transferred. 46 | /// Allows you to supply different values at runtime. Use 0 or a negative number to specify infinite bandwidth. 47 | /// 48 | /// A middleware delegate. 49 | /// getMaxBytesPerSecond 50 | public static MidFunc MaxBandwidthPerRequest(Func getMaxBytesPerSecond) 51 | { 52 | if (getMaxBytesPerSecond == null) 53 | { 54 | throw new ArgumentNullException(nameof(getMaxBytesPerSecond)); 55 | } 56 | 57 | return 58 | next => 59 | async context => 60 | { 61 | var requestBodyStream = context.Request.Body ?? Stream.Null; 62 | var responseBodyStream = context.Response.Body; 63 | 64 | var limitsRequestContext = new RequestContext(context.Request); 65 | 66 | var requestTokenBucket = new FixedTokenBucket( 67 | () => getMaxBytesPerSecond(limitsRequestContext)); 68 | var responseTokenBucket = new FixedTokenBucket( 69 | () => getMaxBytesPerSecond(limitsRequestContext)); 70 | 71 | // ReSharper disable once ArrangeBraces_using 72 | using (requestTokenBucket.RegisterRequest()) 73 | using (responseTokenBucket.RegisterRequest()) 74 | { 75 | 76 | context.Request.Body = new ThrottledStream(requestBodyStream, requestTokenBucket); 77 | context.Response.Body = new ThrottledStream(responseBodyStream, responseTokenBucket); 78 | 79 | //TODO consider SendFile interception 80 | await next(context).ConfigureAwait(false); 81 | } 82 | }; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Limits.MaxConcurrentRequests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using LimitsMiddleware.Logging; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace LimitsMiddleware 8 | { 9 | using MidFunc = Func; 10 | 11 | public static partial class Limits 12 | { 13 | /// 14 | /// Limits the number of concurrent requests that can be handled used by the subsequent stages in the aspnetcore pipeline. 15 | /// 16 | /// 17 | /// The maximum number of concurrent requests. Use 0 or a negative 18 | /// number to specify unlimited number of concurrent requests. 19 | /// 20 | /// (Optional) The name of the logger log messages are written to. 21 | /// A middleware delegate. 22 | public static MidFunc MaxConcurrentRequests(int maxConcurrentRequests, string loggerName = null) => 23 | MaxConcurrentRequests(() => maxConcurrentRequests, loggerName); 24 | 25 | /// 26 | /// Limits the number of concurrent requests that can be handled used by the subsequent stages in the aspnetcore pipeline. 27 | /// 28 | /// 29 | /// A delegate to retrieve the maximum number of concurrent requests. Allows you 30 | /// to supply different values at runtime. Use 0 or a negative number to specify unlimited number of concurrent 31 | /// requests. 32 | /// 33 | /// (Optional) The name of the logger log messages are written to. 34 | /// A middleware delegate. 35 | /// getMaxConcurrentRequests 36 | public static MidFunc MaxConcurrentRequests(Func getMaxConcurrentRequests, string loggerName = null) 37 | { 38 | if (getMaxConcurrentRequests == null) 39 | { 40 | throw new ArgumentNullException(nameof(getMaxConcurrentRequests)); 41 | } 42 | 43 | return MaxConcurrentRequests(_ => getMaxConcurrentRequests(), loggerName); 44 | } 45 | 46 | /// 47 | /// Limits the number of concurrent requests that can be handled used by the subsequent stages in the aspnetcore pipeline. 48 | /// 49 | /// 50 | /// A delegate to retrieve the maximum number of concurrent requests. Allows you 51 | /// to supply different values at runtime. Use 0 or a negative number to specify unlimited number of concurrent 52 | /// requests. 53 | /// 54 | /// (Optional) The name of the logger log messages are written to. 55 | /// A middleware delegate. 56 | /// getMaxConcurrentRequests 57 | public static MidFunc MaxConcurrentRequests(Func getMaxConcurrentRequests, string loggerName = null) 58 | { 59 | if (getMaxConcurrentRequests == null) 60 | { 61 | throw new ArgumentNullException(nameof(getMaxConcurrentRequests)); 62 | } 63 | 64 | loggerName = string.IsNullOrWhiteSpace(loggerName) 65 | ? $"{nameof(LimitsMiddleware)}.{nameof(MaxConcurrentRequests)}" 66 | : loggerName; 67 | var logger = LogProvider.GetLogger(loggerName); 68 | int concurrentRequestCounter = 0; 69 | 70 | return 71 | next => 72 | async context => 73 | { 74 | var limitsRequestContext = new RequestContext(context.Request); 75 | int maxConcurrentRequests = getMaxConcurrentRequests(limitsRequestContext); 76 | if (maxConcurrentRequests <= 0) 77 | { 78 | maxConcurrentRequests = int.MaxValue; 79 | } 80 | try 81 | { 82 | int concurrentRequests = Interlocked.Increment(ref concurrentRequestCounter); 83 | logger.Debug($"Concurrent request {concurrentRequests}/{maxConcurrentRequests}."); 84 | if (concurrentRequests > maxConcurrentRequests) 85 | { 86 | logger.Info($"Limit ({maxConcurrentRequests}). Request rejected."); 87 | var response = context.Response; 88 | response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; 89 | return; 90 | } 91 | await next(context); 92 | } 93 | finally 94 | { 95 | int concurrentRequests = Interlocked.Decrement(ref concurrentRequestCounter); 96 | logger.Debug($"Concurrent request {concurrentRequests}/{maxConcurrentRequests}."); 97 | } 98 | }; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Limits.MaxQueryStringLength.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using LimitsMiddleware.Logging; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace LimitsMiddleware 7 | { 8 | using MidFunc = Func; 9 | 10 | public static partial class Limits 11 | { 12 | /// 13 | /// Limits the length of the query string. 14 | /// 15 | /// Maximum length of the query string. 16 | /// (Optional) The name of the logger log messages are written to. 17 | /// A middleware delegate. 18 | public static MidFunc MaxQueryStringLength(int maxQueryStringLength, string loggerName = null) => 19 | MaxQueryStringLength(_ => maxQueryStringLength, loggerName); 20 | 21 | /// 22 | /// Limits the length of the query string. 23 | /// 24 | /// A delegate to get the maximum query string length. 25 | /// (Optional) The name of the logger log messages are written to. 26 | /// A middleware delegate. 27 | public static MidFunc MaxQueryStringLength(Func getMaxQueryStringLength, string loggerName = null) => 28 | MaxQueryStringLength(_ => getMaxQueryStringLength(), loggerName); 29 | 30 | /// 31 | /// Limits the length of the query string. 32 | /// 33 | /// A delegate to get the maximum query string length. 34 | /// (Optional) The name of the logger log messages are written to. 35 | /// A middleware delegate. 36 | /// getMaxQueryStringLength 37 | public static MidFunc MaxQueryStringLength(Func getMaxQueryStringLength, string loggerName = null) 38 | { 39 | if (getMaxQueryStringLength == null) 40 | { 41 | throw new ArgumentNullException(nameof(getMaxQueryStringLength)); 42 | } 43 | 44 | loggerName = string.IsNullOrWhiteSpace(loggerName) 45 | ? $"{nameof(LimitsMiddleware)}.{nameof(MaxQueryStringLength)}" 46 | : loggerName; 47 | var logger = LogProvider.GetLogger(loggerName); 48 | 49 | return 50 | next => 51 | async context => 52 | { 53 | var requestContext = new RequestContext(context.Request); 54 | 55 | var queryString = context.Request.QueryString; 56 | if (queryString.HasValue) 57 | { 58 | int maxQueryStringLength = getMaxQueryStringLength(requestContext); 59 | string unescapedQueryString = Uri.UnescapeDataString(queryString.Value); 60 | logger.Debug($"Querystring of request with an unescaped length of {unescapedQueryString.Length}"); 61 | if (unescapedQueryString.Length > maxQueryStringLength) 62 | { 63 | logger.Info($"Querystring (Length {unescapedQueryString.Length}) too long (allowed {maxQueryStringLength}). Request rejected."); 64 | context.Response.StatusCode = (int)HttpStatusCode.RequestUriTooLong; 65 | return; 66 | } 67 | } 68 | else 69 | { 70 | logger.Debug("No querystring."); 71 | } 72 | await next(context); 73 | }; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Limits.MaxRequestContentLength.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using LimitsMiddleware.Logging; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace LimitsMiddleware 7 | { 8 | using MidFunc = Func; 9 | 10 | public static partial class Limits 11 | { 12 | /// 13 | /// Limits the length of the request content. 14 | /// 15 | /// Maximum length of the content. 16 | /// (Optional) The name of the logger log messages are written to. 17 | /// A middleware delegate. 18 | public static MidFunc MaxRequestContentLength(int maxContentLength, string loggerName = null) 19 | { 20 | return MaxRequestContentLength(() => maxContentLength, loggerName); 21 | } 22 | 23 | /// 24 | /// Limits the length of the request content. 25 | /// 26 | /// A delegate to get the maximum content length. 27 | /// (Optional) The name of the logger log messages are written to. 28 | /// A middleware delegate. 29 | /// getMaxContentLength 30 | public static MidFunc MaxRequestContentLength(Func getMaxContentLength, string loggerName = null) 31 | { 32 | if (getMaxContentLength == null) 33 | { 34 | throw new ArgumentNullException(nameof(getMaxContentLength)); 35 | } 36 | 37 | return MaxRequestContentLength(_ => getMaxContentLength(), loggerName); 38 | } 39 | 40 | /// 41 | /// Limits the length of the request content. 42 | /// 43 | /// A delegate to get the maximum content length. 44 | /// (Optional) The name of the logger log messages are written to. 45 | /// A middleware delegate. 46 | /// getMaxContentLength 47 | public static MidFunc MaxRequestContentLength(Func getMaxContentLength, string loggerName = null) 48 | { 49 | if (getMaxContentLength == null) 50 | { 51 | throw new ArgumentNullException(nameof(getMaxContentLength)); 52 | } 53 | 54 | loggerName = string.IsNullOrWhiteSpace(loggerName) 55 | ? $"{nameof(LimitsMiddleware)}.{nameof(MaxRequestContentLength)}" 56 | : loggerName; 57 | var logger = LogProvider.GetLogger(loggerName); 58 | 59 | return 60 | next => 61 | async context => 62 | { 63 | var request = context.Request; 64 | string requestMethod = request.Method.Trim().ToUpperInvariant(); 65 | 66 | if (requestMethod == "HEAD") 67 | { 68 | logger.Debug("HEAD request forwarded without check."); 69 | await next(context); 70 | } 71 | int maxContentLength = getMaxContentLength(new RequestContext(request)); 72 | logger.Debug($"Max valid content length is {maxContentLength}."); 73 | if (!IsChunkedRequest(request)) 74 | { 75 | logger.Debug("Not a chunked request. Checking content length header."); 76 | var contentLengthHeaderValue = request.ContentLength; 77 | if (!contentLengthHeaderValue.HasValue) 78 | { 79 | if (requestMethod == "PUT" || requestMethod == "POST") 80 | { 81 | SetResponseStatusCode(context, (int)HttpStatusCode.LengthRequired); 82 | return; 83 | } 84 | request.Body = new ContentLengthLimitingStream(request.Body, 0); 85 | } 86 | else 87 | { 88 | if (contentLengthHeaderValue > maxContentLength) 89 | { 90 | logger.Info($"Content length of {contentLengthHeaderValue} exceeds maximum of {maxContentLength}. Request rejected."); 91 | SetResponseStatusCode(context, (int)HttpStatusCode.RequestEntityTooLarge); 92 | return; 93 | } 94 | logger.Debug("Content length header check passed."); 95 | 96 | request.Body = new ContentLengthLimitingStream(request.Body, maxContentLength); 97 | logger.Debug($"Request body stream configured with length limiting stream of {maxContentLength}."); 98 | } 99 | } 100 | else 101 | { 102 | request.Body = new ContentLengthLimitingStream(request.Body, maxContentLength); 103 | logger.Debug($"Request body stream configured with length limiting stream of {maxContentLength}."); 104 | logger.Debug("Chunked request. Content length header not checked."); 105 | } 106 | 107 | try 108 | { 109 | logger.Debug("Request forwarded."); 110 | await next(context); 111 | logger.Debug("Processing finished."); 112 | } 113 | catch (ContentLengthRequiredException) 114 | { 115 | logger.Info("Content length required. Request canceled and rejected."); 116 | SetResponseStatusCode(context, (int)HttpStatusCode.LengthRequired); 117 | } 118 | catch (ContentLengthExceededException) 119 | { 120 | logger.Info($"Content length of {maxContentLength} exceeded. Request canceled and rejected."); 121 | SetResponseStatusCode(context, (int)HttpStatusCode.RequestEntityTooLarge); 122 | } 123 | }; 124 | } 125 | 126 | private static bool IsChunkedRequest(HttpRequest request) 127 | { 128 | string header = request.Headers["Transfer-Encoding"]; 129 | return header != null && header.Equals("chunked", StringComparison.OrdinalIgnoreCase); 130 | } 131 | 132 | private static void SetResponseStatusCode(HttpContext context, int statusCode) 133 | { 134 | context.Response.StatusCode = statusCode; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Limits.MaxUrlLength.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using LimitsMiddleware.Logging; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Http.Extensions; 6 | 7 | namespace LimitsMiddleware 8 | { 9 | using MidFunc = Func; 10 | 11 | public static partial class Limits 12 | { 13 | public static MidFunc MaxUrlLength(int maxUrlLength, string loggerName = null) => MaxUrlLength(() => maxUrlLength, loggerName); 14 | 15 | public static MidFunc MaxUrlLength(Func getMaxUrlLength, string loggerName = null) => MaxUrlLength(_ => getMaxUrlLength(), loggerName); 16 | 17 | public static MidFunc MaxUrlLength(Func getMaxUrlLength, string loggerName = null) 18 | { 19 | if (getMaxUrlLength == null) 20 | { 21 | throw new ArgumentNullException(nameof(getMaxUrlLength)); 22 | } 23 | 24 | loggerName = string.IsNullOrWhiteSpace(loggerName) 25 | ? $"{nameof(LimitsMiddleware)}.{nameof(MaxUrlLength)}" 26 | : loggerName; 27 | 28 | var logger = LogProvider.GetLogger(loggerName); 29 | 30 | return 31 | next => 32 | async context => 33 | { 34 | int maxUrlLength = getMaxUrlLength(new RequestContext(context.Request)); 35 | string unescapedUri = Uri.UnescapeDataString(new Uri(context.Request.GetEncodedUrl()).AbsoluteUri); 36 | 37 | logger.Debug("Checking request url length."); 38 | if (unescapedUri.Length > maxUrlLength) 39 | { 40 | logger.Info($"Url \"{unescapedUri}\"(Length: {unescapedUri.Length}) exceeds allowed length of {maxUrlLength}. Request rejected."); 41 | context.Response.StatusCode = (int)HttpStatusCode.RequestUriTooLong; 42 | } 43 | logger.Debug("Check passed. Request forwarded."); 44 | await next(context); 45 | }; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Limits.MinResponseDelay.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using LimitsMiddleware.Logging; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace LimitsMiddleware 7 | { 8 | using MidFunc = Func; 9 | 10 | public static partial class Limits 11 | { 12 | /// 13 | /// Adds a minimum delay before sending the response. 14 | /// 15 | /// The min response delay, in milliseconds. 16 | /// (Optional) The name of the logger log messages are written to. 17 | /// A midfunc. 18 | public static MidFunc MinResponseDelay(int minDelay, string loggerName = null) => MinResponseDelay(_ => minDelay, loggerName); 19 | 20 | /// 21 | /// Adds a minimum delay before sending the response. 22 | /// 23 | /// A delegate to return the min response delay. 24 | /// (Optional) The name of the logger log messages are written to. 25 | /// The aspnetcore builder instance. 26 | /// getMinDelay 27 | public static MidFunc MinResponseDelay(Func getMinDelay, string loggerName = null) 28 | { 29 | if (getMinDelay == null) 30 | { 31 | throw new ArgumentNullException(nameof(getMinDelay)); 32 | } 33 | 34 | return MinResponseDelay(_ => getMinDelay(), loggerName); 35 | } 36 | 37 | /// 38 | /// Adds a minimum delay before sending the response. 39 | /// 40 | /// A delegate to return the min response delay. 41 | /// (Optional) The name of the logger log messages are written to. 42 | /// The aspnetcore builder instance. 43 | /// getMinDelay 44 | public static MidFunc MinResponseDelay(Func getMinDelay, string loggerName = null) 45 | { 46 | if (getMinDelay == null) 47 | { 48 | throw new ArgumentNullException(nameof(getMinDelay)); 49 | } 50 | 51 | return MinResponseDelay(context => TimeSpan.FromMilliseconds(getMinDelay(context)), loggerName); 52 | } 53 | 54 | /// 55 | /// Adds a minimum delay before sending the response. 56 | /// 57 | /// The min response delay. 58 | /// (Optional) The name of the logger log messages are written to. 59 | /// A midfunc. 60 | public static MidFunc MinResponseDelay(TimeSpan minDelay, string loggerName = null) => MinResponseDelay(_ => minDelay, loggerName); 61 | 62 | /// 63 | /// Adds a minimum delay before sending the response. 64 | /// 65 | /// A delegate to return the min response delay. 66 | /// (Optional) The name of the logger log messages are written to. 67 | /// The aspnetcore builder instance. 68 | /// getMinDelay 69 | public static MidFunc MinResponseDelay(Func getMinDelay, string loggerName = null) 70 | { 71 | if (getMinDelay == null) 72 | { 73 | throw new ArgumentNullException(nameof(getMinDelay)); 74 | } 75 | 76 | return MinResponseDelay(_ => getMinDelay(), loggerName); 77 | } 78 | 79 | /// 80 | /// Adds a minimum delay before sending the response. 81 | /// 82 | /// A delegate to return the min response delay. 83 | /// (Optional) The name of the logger log messages are written to. 84 | /// The aspnetcore builder instance. 85 | /// getMinDelay 86 | public static MidFunc MinResponseDelay(Func getMinDelay, string loggerName = null) 87 | { 88 | if (getMinDelay == null) 89 | { 90 | throw new ArgumentNullException(nameof(getMinDelay)); 91 | } 92 | 93 | loggerName = string.IsNullOrWhiteSpace(loggerName) 94 | ? $"{nameof(LimitsMiddleware)}.{nameof(MinResponseDelay)}" 95 | : loggerName; 96 | 97 | var logger = LogProvider.GetLogger(loggerName); 98 | 99 | return 100 | next => 101 | async context => 102 | { 103 | var limitsRequestContext = new RequestContext(context.Request); 104 | var delay = getMinDelay(limitsRequestContext); 105 | 106 | if (delay <= TimeSpan.Zero) 107 | { 108 | await next(context); 109 | return; 110 | } 111 | 112 | logger.Debug($"Delaying response by {delay}"); 113 | await Task.Delay(delay, context.RequestAborted); 114 | await next(context); 115 | }; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/LimitsMiddleware.AspNetCore.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | a61b64d6-3d62-472e-b9ba-2435166c6786 11 | LimitsMiddleware 12 | .\obj 13 | .\bin\ 14 | v4.5.2 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/LimitsMiddleware.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 10 | a61b64d6-3d62-472e-b9ba-2435166c6786 11 | LimitsMiddleware 12 | .\obj 13 | .\bin\ 14 | v4.5.2 15 | 16 | 17 | 18 | 2.0 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("LimitsMiddleware")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("a61b64d6-3d62-472e-b9ba-2435166c6786")] 20 | [assembly:InternalsVisibleTo("LimitsMiddleware.AspNetCore.Tests")] 21 | -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/RateLimiters/FixedTokenBucket.cs: -------------------------------------------------------------------------------- 1 | using LimitsMiddleware.Logging.LogProviders; 2 | 3 | namespace LimitsMiddleware.RateLimiters 4 | { 5 | using System; 6 | using System.Threading; 7 | 8 | internal class FixedTokenBucket 9 | { 10 | private readonly Func _getBucketTokenCapacty; 11 | private readonly long _refillIntervalTicks; 12 | private readonly TimeSpan _refillInterval; 13 | private readonly GetUtcNow _getUtcNow; 14 | private long _nextRefillTime; 15 | private long _tokens; 16 | private readonly InterlockedBoolean _updatingTokens = new InterlockedBoolean(); 17 | private int _concurrentRequestCount; 18 | 19 | public FixedTokenBucket(Func getBucketTokenCapacty, GetUtcNow getUtcNow = null) 20 | { 21 | _getBucketTokenCapacty = getBucketTokenCapacty; 22 | _refillInterval = TimeSpan.FromSeconds(1); 23 | _refillIntervalTicks = TimeSpan.FromSeconds(1).Ticks; 24 | _getUtcNow = getUtcNow ?? SystemClock.GetUtcNow; 25 | } 26 | 27 | public bool ShouldThrottle(int tokenCount) 28 | { 29 | TimeSpan _; 30 | return ShouldThrottle(tokenCount, out _); 31 | } 32 | 33 | public bool ShouldThrottle(int tokenCount, out TimeSpan waitTimeSpan) 34 | { 35 | waitTimeSpan = TimeSpan.Zero; 36 | UpdateTokens(); 37 | long tokens = Interlocked.Read(ref _tokens); 38 | if (tokens < tokenCount) 39 | { 40 | long currentTime = _getUtcNow().Ticks; 41 | long waitTicks = _nextRefillTime - currentTime; 42 | if (waitTicks <= 0) 43 | { 44 | return false; 45 | } 46 | waitTimeSpan = TimeSpan.FromTicks(waitTicks * _concurrentRequestCount); 47 | return true; 48 | } 49 | Interlocked.Add(ref _tokens, -tokenCount); 50 | return false; 51 | } 52 | 53 | public long CurrentTokenCount 54 | { 55 | get 56 | { 57 | UpdateTokens(); 58 | return Interlocked.Read(ref _tokens); 59 | } 60 | } 61 | 62 | public int Capacity => _getBucketTokenCapacty(); 63 | 64 | public double Rate => Capacity/_refillInterval.TotalSeconds; 65 | 66 | public IDisposable RegisterRequest() 67 | { 68 | Interlocked.Increment(ref _concurrentRequestCount); 69 | return new DisposableAction(() => 70 | { 71 | Interlocked.Decrement(ref _concurrentRequestCount); 72 | }); 73 | } 74 | 75 | private void UpdateTokens() 76 | { 77 | if (_updatingTokens.EnsureCalledOnce()) 78 | { 79 | return; 80 | } 81 | long currentTime = _getUtcNow().Ticks; 82 | 83 | if (currentTime >= _nextRefillTime) 84 | { 85 | Interlocked.Exchange(ref _tokens, _getBucketTokenCapacty()); 86 | _nextRefillTime = currentTime + _refillIntervalTicks; 87 | } 88 | 89 | _updatingTokens.Set(false); 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/RateLimiters/GetUtcNow.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware.RateLimiters 2 | { 3 | using System; 4 | 5 | internal delegate DateTimeOffset GetUtcNow(); 6 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/RateLimiters/SystemClock.cs: -------------------------------------------------------------------------------- 1 | // https://github.com/robertmircea/RateLimiters 2 | 3 | namespace LimitsMiddleware.RateLimiters 4 | { 5 | using System; 6 | 7 | internal static class SystemClock 8 | { 9 | internal static readonly GetUtcNow GetUtcNow = () => DateTimeOffset.UtcNow; 10 | } 11 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/RequestContext.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware 2 | { 3 | using System; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Http.Extensions; 6 | 7 | public class RequestContext 8 | { 9 | private readonly HttpRequest _request; 10 | 11 | internal RequestContext(HttpRequest request) 12 | { 13 | _request = request; 14 | } 15 | 16 | public string Method => _request.Method; 17 | 18 | public Uri Uri => new Uri(_request.GetEncodedUrl()); 19 | 20 | public IHeaderDictionary Headers => _request.Headers; 21 | 22 | public string Host => _request.Host.ToString(); 23 | 24 | //public string LocalIpAddress => _request.LocalIpAddress; 25 | 26 | //public int? LocalPort => _request.LocalPort; 27 | 28 | //public string RemoteIpAddress => _request.RemoteIpAddress; 29 | 30 | //public int? RemotePort => _request.RemotePort; 31 | 32 | //public ClaimsPrincipal User => _request.User; 33 | } 34 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/ThrottledStream.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using RateLimiters; 8 | 9 | internal class ThrottledStream : Stream 10 | { 11 | private readonly Stream _innerStream; 12 | private readonly FixedTokenBucket _tokenBucket; 13 | 14 | public ThrottledStream(Stream innerStream, FixedTokenBucket fixedTokenBucket) 15 | { 16 | if (innerStream == null) 17 | { 18 | throw new ArgumentNullException(nameof(innerStream)); 19 | } 20 | 21 | _innerStream = innerStream; 22 | _tokenBucket = fixedTokenBucket; 23 | } 24 | 25 | public override bool CanRead => _innerStream.CanRead; 26 | 27 | public override bool CanSeek => _innerStream.CanSeek; 28 | 29 | public override bool CanWrite => _innerStream.CanWrite; 30 | 31 | public override long Length => _innerStream.Length; 32 | 33 | public override long Position 34 | { 35 | get { return _innerStream.Position; } 36 | set { _innerStream.Position = value; } 37 | } 38 | 39 | public override void Flush() 40 | { 41 | _innerStream.Flush(); 42 | } 43 | 44 | protected override void Dispose(bool disposing) 45 | { 46 | if (disposing) 47 | { 48 | _innerStream.Dispose(); 49 | } 50 | } 51 | 52 | public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); 53 | 54 | public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); 55 | 56 | public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 57 | { 58 | int rateBytesPerSecond = Convert.ToInt32(_tokenBucket.Rate); 59 | if (rateBytesPerSecond <= 0) 60 | { 61 | _innerStream.Write(buffer, offset, count); 62 | return; 63 | } 64 | 65 | int tempCountBytes = count; 66 | 67 | // In the unlikely event that the count is greater than the rate (i.e. buffer 68 | // is 16KB (typically this is the max size) and the rate is < 16Kbs), we'll need 69 | // to split it into multiple. 70 | 71 | TimeSpan wait; 72 | while (tempCountBytes > rateBytesPerSecond) 73 | { 74 | if (_tokenBucket.ShouldThrottle(tempCountBytes, out wait)) 75 | { 76 | if (wait > TimeSpan.Zero) 77 | { 78 | await Task.Delay(wait, cancellationToken).ConfigureAwait(false); 79 | } 80 | } 81 | await _innerStream.WriteAsync(buffer, offset, rateBytesPerSecond, cancellationToken) 82 | .ConfigureAwait(false); 83 | offset += rateBytesPerSecond; 84 | tempCountBytes -= rateBytesPerSecond; 85 | } 86 | while (_tokenBucket.ShouldThrottle(tempCountBytes, out wait)) 87 | { 88 | if (wait > TimeSpan.Zero) 89 | { 90 | await Task.Delay(wait, cancellationToken).ConfigureAwait(false); 91 | } 92 | } 93 | await _innerStream.WriteAsync(buffer, offset, tempCountBytes, cancellationToken) 94 | .ConfigureAwait(false); 95 | } 96 | 97 | public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => _innerStream.ReadAsync(buffer, offset, count, cancellationToken); 98 | 99 | public override void SetLength(long value) 100 | { 101 | _innerStream.SetLength(value); 102 | } 103 | 104 | public override void Write(byte[] buffer, int offset, int count) 105 | { 106 | WriteAsync(buffer, offset, count).Wait(); 107 | } 108 | 109 | public override string ToString() => _innerStream.ToString(); 110 | } 111 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/TimeoutStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using LimitsMiddleware.Logging; 6 | 7 | namespace LimitsMiddleware 8 | { 9 | internal class TimeoutStream : Stream 10 | { 11 | private readonly Stream _innerStream; 12 | private readonly TimeSpan _timeout; 13 | private readonly ILog _logger; 14 | private readonly Action _timerCallback; 15 | private Timer _timer; 16 | 17 | public TimeoutStream(Stream innerStream, TimeSpan timeout, ILog logger) 18 | { 19 | _innerStream = innerStream; 20 | _timeout = timeout; 21 | _logger = logger; 22 | 23 | _timerCallback = state => 24 | { 25 | _logger.Info($"Timeout of {_timeout} reached."); 26 | Dispose(); 27 | }; 28 | _timer = StartTimer(); 29 | } 30 | 31 | private Timer StopTimer() => null; 32 | 33 | private Timer StartTimer() => new Timer(state => _timerCallback(state), null, 0, (int)_timeout.TotalMilliseconds); 34 | 35 | public override bool CanRead => _innerStream.CanRead; 36 | 37 | public override bool CanSeek => _innerStream.CanSeek; 38 | 39 | public override bool CanWrite => _innerStream.CanWrite; 40 | 41 | public override long Length => _innerStream.Length; 42 | 43 | public override long Position 44 | { 45 | get { return _innerStream.Position; } 46 | set { _innerStream.Position = value; } 47 | } 48 | 49 | public override void Flush() 50 | { 51 | _innerStream.Flush(); 52 | } 53 | 54 | public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); 55 | 56 | public override void SetLength(long value) 57 | { 58 | _innerStream.SetLength(value); 59 | } 60 | 61 | public override int Read(byte[] buffer, int offset, int count) => _innerStream.Read(buffer, offset, count); 62 | 63 | public override void Write(byte[] buffer, int offset, int count) 64 | { 65 | _innerStream.Write(buffer, offset, count); 66 | } 67 | 68 | protected override void Dispose(bool disposing) 69 | { 70 | if (disposing) 71 | { 72 | _timer?.Dispose(); 73 | //_innerStream?.Dispose(); 74 | } 75 | } 76 | 77 | public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => _innerStream?.CopyToAsync(destination, bufferSize, cancellationToken); 78 | 79 | public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream?.FlushAsync(cancellationToken); 80 | 81 | public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 82 | { 83 | int read = await _innerStream.ReadAsync(buffer, offset, count, cancellationToken); 84 | Reset(); 85 | return read; 86 | } 87 | 88 | public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 89 | { 90 | await _innerStream.WriteAsync(buffer, offset, count, cancellationToken); 91 | Reset(); 92 | } 93 | 94 | private void Reset() 95 | { 96 | _timer = StopTimer(); 97 | _logger.Debug("Timeout timer reset."); 98 | _timer = StartTimer(); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/LimitsMiddleware.AspNetCore/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Limits Middleware for ASP.NET Core", 3 | "version": "1.0.0-alpha", 4 | "authors": [ "Luk Vermeulen" ], 5 | "copyright": "Copyright 2016", 6 | "description": "Middleware that applies limits to an aspnetcore pipeline. Currently supported are max bandwidth, max concurrent requests, connection timeout, max request content length, max querystring length and max url length.", 7 | 8 | "buildOptions": { 9 | "define": [ "LIBLOG_PORTABLE" ], 10 | "optimize": true, 11 | "outputName": "LimitsMiddlewareAspNetCore", 12 | "xmlDoc": false 13 | }, 14 | 15 | "packOptions": { 16 | "owners": [ "Luk Vermeulen" ], 17 | "requireLicenseAcceptance": false, 18 | "summary": "Limits Middleware for ASP.NET Core", 19 | "tags": [ "middleware", "throttling", "limits", "aspnetcore" ], 20 | "projectUrl": "https://github.com/lvermeulen/LimitsMiddleware.aspnetcore", 21 | "licenseUrl": "https://github.com/lvermeulen/LimitsMiddleware.aspnetcore/blob/master/LICENSE", 22 | "iconUrl": "http://i.imgur.com/D7PUb42.png?1", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/lvermeulen/LimitsMiddleware.aspnetcore" 26 | } 27 | }, 28 | 29 | "dependencies": { 30 | "Microsoft.AspNetCore.Http.Abstractions": "1.0.0", 31 | "Microsoft.AspNetCore.Http": "1.0.0", 32 | "Microsoft.AspNetCore.Http.Extensions": "1.0.0", 33 | "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0" 34 | }, 35 | 36 | "frameworks": { 37 | "net451": { }, 38 | "netstandard1.6": { 39 | "dependencies": { 40 | "NETStandard.Library": "1.6.0", 41 | "Microsoft.CSharp": "4.0.1", 42 | "System.Dynamic.Runtime": "4.0.11", 43 | "System.Globalization.Extensions": "4.0.1", 44 | "System.Security.Claims": "4.0.1" 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/ConnectionTimeoutTests.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware.Tests 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Net.Http; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Extensions; 11 | using Microsoft.AspNetCore.Builder; 12 | using Microsoft.AspNetCore.Hosting; 13 | using Microsoft.AspNetCore.TestHost; 14 | using Shouldly; 15 | using Xunit; 16 | 17 | public class ConnectionTimeoutTests 18 | { 19 | [Fact] 20 | public void When_time_out_delegate_is_null_then_should_throw() 21 | { 22 | Action act = () => Limits.ConnectionTimeout((Func)null); 23 | 24 | act.ShouldThrow(); 25 | } 26 | 27 | [Fact(Skip = "TimeoutStream not working correctly")] 28 | public async Task When_time_out_is_triggered_then_should_throw() 29 | { 30 | TimeSpan timeout = TimeSpan.FromMilliseconds(1000); 31 | Func getConnectionTimeout = () => timeout; 32 | HttpClient httpClient = CreateHttpClient(getConnectionTimeout); 33 | 34 | var stream = new DelayedReadStream(timeout.Add(TimeSpan.FromMilliseconds(500))); 35 | stream.Write(new byte[2048], 0, 2048); 36 | stream.Position = 0; 37 | await Assert.ThrowsAsync(() => httpClient.PostAsync("http://example.com", new StreamContent(stream))); 38 | } 39 | 40 | [Fact] 41 | public async Task When_time_out_is_not_triggered_then_get_OK() 42 | { 43 | TimeSpan timeout = TimeSpan.FromMilliseconds(1000); 44 | Func getConnectionTimeout = () => timeout; 45 | HttpClient httpClient = CreateHttpClient(getConnectionTimeout); 46 | 47 | var stream = new DelayedReadStream(timeout.Add(TimeSpan.FromMilliseconds(-500))); 48 | stream.Write(new byte[2048], 0, 2048); 49 | stream.Position = 0; 50 | HttpResponseMessage response = await httpClient.PostAsync("http://example.com", new StreamContent(stream)); 51 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 52 | } 53 | 54 | private static HttpClient CreateHttpClient(Func getConnectionTimeout) => CreateHttpClient(_ => getConnectionTimeout()); 55 | 56 | private static HttpClient CreateHttpClient(Func getConnectionTimeout) 57 | { 58 | var builder = new WebHostBuilder().Configure(app => app 59 | .ConnectionTimeout(getConnectionTimeout) 60 | .Use(async (context, _) => 61 | { 62 | var buffer = new byte[1024]; 63 | await context.Request.Body.ReadAsync(buffer, 0, buffer.Length); 64 | byte[] bytes = Enumerable.Repeat((byte)0x1, 1024).ToArray(); 65 | context.Response.StatusCode = (int)HttpStatusCode.OK; 66 | context.Response.ContentLength = bytes.Length; 67 | context.Response.ContentType = "application/octet-stream"; 68 | await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); 69 | })); 70 | 71 | var server = new TestServer(builder); 72 | return server.CreateClient(); 73 | } 74 | 75 | private class DelayedReadStream : MemoryStream 76 | { 77 | private readonly TimeSpan _delay; 78 | 79 | public DelayedReadStream(TimeSpan delay) 80 | { 81 | _delay = delay; 82 | } 83 | 84 | public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 85 | { 86 | await Task.Delay(_delay, cancellationToken); 87 | return await base.ReadAsync(buffer, offset, count, cancellationToken); 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/Files/file_64KB.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/Helpers/EapTask.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware.Tests.Helpers 2 | { 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | // http://stackoverflow.com/a/22786826 8 | 9 | public sealed class EapTask 10 | where TEventHandler: class 11 | { 12 | private readonly TaskCompletionSource _completionSource; 13 | private readonly TEventHandler _eventHandler; 14 | 15 | public EapTask(TaskCompletionSource completionSource, TEventHandler eventHandler) 16 | { 17 | _completionSource = completionSource; 18 | _eventHandler = eventHandler; 19 | } 20 | 21 | public Task Start(Action subscribe, Action unsubscribe) => Start(subscribe, unsubscribe, CancellationToken.None); 22 | 23 | public async Task Start(Action subscribe, Action unsubscribe, CancellationToken cancellationToken) 24 | { 25 | subscribe(_eventHandler); 26 | try 27 | { 28 | using (cancellationToken.Register(() => _completionSource.SetCanceled())) 29 | { 30 | return await _completionSource.Task; 31 | } 32 | } 33 | finally 34 | { 35 | unsubscribe(_eventHandler); 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/Helpers/TaskExt.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware.Tests.Helpers 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | 6 | // http://stackoverflow.com/a/22786826 7 | 8 | public static class TaskExt 9 | { 10 | public static EapTask> FromEvent() 11 | { 12 | var tcs = new TaskCompletionSource(); 13 | var handler = new EventHandler((_, eventArgs) => tcs.TrySetResult(eventArgs)); 14 | return new EapTask>(tcs, handler); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/HttpResponseExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace LimitsMiddleware.Tests 4 | { 5 | public static class HttpResponseExtensions 6 | { 7 | public static bool IsSuccessStatusCode(this HttpResponse response) => (response.StatusCode >= 200) && (response.StatusCode <= 299); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/LimitsMiddleware.AspNetCore.Tests.xproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 1e42d5f7-31eb-4c2b-afd6-841d2024e636 10 | LimitsMiddleware.Tests 11 | .\obj 12 | .\bin\ 13 | v4.5.2 14 | 15 | 16 | 2.0 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/MaxBandwidthGlobalTests.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware.Tests 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Net.Http; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Extensions; 11 | using Microsoft.AspNetCore.Builder; 12 | using Microsoft.AspNetCore.Hosting; 13 | using Microsoft.AspNetCore.TestHost; 14 | using Shouldly; 15 | using Xunit; 16 | 17 | public class MaxBandwidthGlobalTests 18 | { 19 | [Fact] 20 | public async Task When_bandwidth_is_applied_then_time_to_receive_data_should_be_longer_for_multiple_concurrent_requests() 21 | { 22 | int bandwidth = -1; 23 | // ReSharper disable once AccessToModifiedClosure - yeah we want to modify it... 24 | Func getMaxBandwidth = () => bandwidth; 25 | var stopwatch = new Stopwatch(); 26 | 27 | using (HttpClient httpClient = CreateHttpClient(getMaxBandwidth, 2)) 28 | { 29 | stopwatch.Start(); 30 | 31 | await Task.WhenAll(httpClient.GetAsync("/"), httpClient.GetAsync("/")); 32 | 33 | stopwatch.Stop(); 34 | } 35 | TimeSpan nolimitTimeSpan = stopwatch.Elapsed; 36 | 37 | bandwidth = 2000; 38 | using (HttpClient httpClient = CreateHttpClient(getMaxBandwidth, 2)) 39 | { 40 | stopwatch.Restart(); 41 | 42 | await Task.WhenAll(httpClient.GetAsync("/"), httpClient.GetAsync("/")); 43 | 44 | stopwatch.Stop(); 45 | } 46 | TimeSpan limitedTimeSpan = stopwatch.Elapsed; 47 | 48 | Console.WriteLine(nolimitTimeSpan); 49 | Console.WriteLine(limitedTimeSpan); 50 | 51 | limitedTimeSpan.ShouldBeGreaterThan(nolimitTimeSpan); 52 | } 53 | 54 | [Fact] 55 | public async Task When_bandwidth_is_applied_then_time_to_receive_data_should_be_longer_for_multiple_requests() 56 | { 57 | int bandwidth = -1; 58 | // ReSharper disable once AccessToModifiedClosure - yeah we want to modify it... 59 | Func getMaxBandwidth = () => bandwidth; 60 | 61 | var stopwatch = new Stopwatch(); 62 | using (HttpClient httpClient = CreateHttpClient(getMaxBandwidth, 1)) 63 | { 64 | stopwatch.Start(); 65 | 66 | await httpClient.GetAsync("/"); 67 | await httpClient.GetAsync("/"); 68 | 69 | stopwatch.Stop(); 70 | } 71 | TimeSpan nolimitTimeSpan = stopwatch.Elapsed; 72 | 73 | bandwidth = 1000; 74 | using (HttpClient httpClient = CreateHttpClient(getMaxBandwidth, 1)) 75 | { 76 | stopwatch.Restart(); 77 | 78 | await httpClient.GetAsync("/"); 79 | await httpClient.GetAsync("/"); 80 | 81 | stopwatch.Stop(); 82 | } 83 | TimeSpan limitedTimeSpan = stopwatch.Elapsed; 84 | 85 | Console.WriteLine(nolimitTimeSpan); 86 | Console.WriteLine(limitedTimeSpan); 87 | 88 | limitedTimeSpan.ShouldBeGreaterThan(nolimitTimeSpan); 89 | } 90 | 91 | private static HttpClient CreateHttpClient(Func getMaxBytesPerSecond, int forceConcurrentCount) 92 | { 93 | // This blocks client responses until the number of concurrent clients 94 | // has reached the desired value. 95 | var tcs = new TaskCompletionSource(); 96 | Action requestReceived = () => 97 | { 98 | if (Interlocked.Decrement(ref forceConcurrentCount) == 0) 99 | { 100 | tcs.SetResult(0); 101 | } 102 | }; 103 | 104 | 105 | var builder = new WebHostBuilder().Configure(app => app 106 | .MaxBandwidthGlobal(getMaxBytesPerSecond) 107 | .Use(async (context, _) => 108 | { 109 | requestReceived(); 110 | var delayTask = Task.Delay(5000); 111 | if (await Task.WhenAny(delayTask, tcs.Task) == delayTask) 112 | { 113 | throw new TimeoutException("Timedout waiting for concurrent clients."); 114 | } 115 | 116 | byte[] bytes = Enumerable.Repeat((byte)0x1, 1024).ToArray(); 117 | const int BATCHES = 4; 118 | context.Response.StatusCode = (int)HttpStatusCode.OK; 119 | context.Response.ContentLength = bytes.Length * BATCHES; 120 | context.Response.ContentType = "application/octet-stream"; 121 | for (int i = 0; i < BATCHES; i++) 122 | { 123 | await Task.Delay(1); //forces actual asynchrony 124 | await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); 125 | } 126 | })); 127 | 128 | var server = new TestServer(builder); 129 | return server.CreateClient(); 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/MaxBandwidthPerRequestTests.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware.Tests 2 | { 3 | using System; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Net; 7 | using System.Net.Http; 8 | using System.Reflection; 9 | using System.Threading.Tasks; 10 | using Extensions; 11 | using Microsoft.AspNetCore.Builder; 12 | using Microsoft.AspNetCore.Hosting; 13 | using Microsoft.AspNetCore.Http; 14 | using Microsoft.AspNetCore.TestHost; 15 | using Microsoft.Extensions.FileProviders; 16 | using Shouldly; 17 | using Xunit; 18 | 19 | public class MaxBandwidthPerRequestTests 20 | { 21 | [Theory] 22 | [InlineData("file_64KB.txt", 8000, 15)] 23 | [InlineData("file_64KB.txt", 16000, 5)] 24 | [InlineData("file_512KB.txt", 100000, 5)] 25 | [InlineData("file_512KB.txt", 200000, 2)] 26 | public async Task When_bandwidth_is_applied_then_time_to_receive_data_should_be_longer(string file, int maxKiloBytesPerSeconds, int approximateSeconds) 27 | { 28 | var stopwatch = new Stopwatch(); 29 | using (var httpClient = CreateHttpClient()) 30 | { 31 | stopwatch.Start(); 32 | 33 | var response = await httpClient.GetAsync(file); 34 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 35 | 36 | stopwatch.Stop(); 37 | } 38 | TimeSpan nolimitTimeSpan = stopwatch.Elapsed; 39 | 40 | using (var httpClient = CreateHttpClient(maxKiloBytesPerSeconds)) 41 | { 42 | stopwatch.Restart(); 43 | 44 | var response = await httpClient.GetAsync(file); 45 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 46 | 47 | stopwatch.Stop(); 48 | } 49 | TimeSpan limitedTimeSpan = stopwatch.Elapsed; 50 | 51 | Debug.WriteLine("No limits: {0}", nolimitTimeSpan); 52 | Debug.WriteLine("Limited : {0}", limitedTimeSpan); 53 | 54 | limitedTimeSpan.ShouldBeGreaterThan(nolimitTimeSpan); 55 | 56 | double abs = Math.Abs((limitedTimeSpan.TotalSeconds - nolimitTimeSpan.TotalSeconds) - approximateSeconds); 57 | (abs < 1).ShouldBeTrue($"value {abs} >= 1"); 58 | } 59 | 60 | private static HttpClient CreateHttpClient(int maxKiloBytesPerSecond = -1) 61 | { 62 | string currentDirectory = Path.GetDirectoryName(typeof(MaxBandwidthPerRequestTests).GetTypeInfo().Assembly.Location); 63 | var builder = new WebHostBuilder().Configure(app => app 64 | .MaxBandwidthPerRequest(maxKiloBytesPerSecond) 65 | .UseStaticFiles(new StaticFileOptions() 66 | { 67 | FileProvider = new PhysicalFileProvider(Path.Combine(currentDirectory, "Files")), 68 | RequestPath = new PathString("") 69 | })); 70 | 71 | var server = new TestServer(builder); 72 | return server.CreateClient(); 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/MaxConcurrentRequestsTests.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware.Tests 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Threading.Tasks; 8 | using Extensions; 9 | using Microsoft.AspNetCore.Builder; 10 | using Microsoft.AspNetCore.Hosting; 11 | using Microsoft.AspNetCore.TestHost; 12 | using Shouldly; 13 | using Xunit; 14 | 15 | public class MaxConcurrentRequestsTests 16 | { 17 | [Fact] 18 | public async Task When_max_concurrent_request_is_1_then_second_request_should_get_service_unavailable() 19 | { 20 | var tcs = new TaskCompletionSource(); 21 | var client = CreateHttpClient(1, tcs.Task); 22 | var response1 = await client.GetAsync("/", HttpCompletionOption.ResponseHeadersRead); 23 | var response2 = await client.GetAsync("/", HttpCompletionOption.ResponseHeadersRead); 24 | 25 | tcs.SetResult(0); 26 | 27 | response1.StatusCode.ShouldBe(HttpStatusCode.OK); 28 | response2.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); 29 | } 30 | 31 | [Fact] 32 | public async Task When_max_concurrent_request_is_2_then_second_request_should_get_ok() 33 | { 34 | var tcs = new TaskCompletionSource(); 35 | var client = CreateHttpClient(2, tcs.Task); 36 | var response1 = await client.GetAsync("/", HttpCompletionOption.ResponseHeadersRead); 37 | var response2 = await client.GetAsync("/", HttpCompletionOption.ResponseHeadersRead); 38 | 39 | tcs.SetResult(0); 40 | 41 | response1.StatusCode.ShouldBe(HttpStatusCode.OK); 42 | response2.StatusCode.ShouldBe(HttpStatusCode.OK); 43 | } 44 | 45 | [Fact] 46 | public async Task When_max_concurrent_request_is_0_then_second_request_should_get_ok() 47 | { 48 | var tcs = new TaskCompletionSource(); 49 | var client = CreateHttpClient(0, tcs.Task); 50 | var response1 = await client.GetAsync("/", HttpCompletionOption.ResponseHeadersRead); 51 | var response2 = await client.GetAsync("/", HttpCompletionOption.ResponseHeadersRead); 52 | 53 | tcs.SetResult(0); 54 | 55 | response1.StatusCode.ShouldBe(HttpStatusCode.OK); 56 | response2.StatusCode.ShouldBe(HttpStatusCode.OK); 57 | } 58 | 59 | private static HttpClient CreateHttpClient(int maxConcurrentRequests, Task waitHandle) => CreateHttpClient(_ => maxConcurrentRequests, waitHandle); 60 | 61 | private static HttpClient CreateHttpClient(Func maxConcurrentRequests, Task waitHandle) 62 | { 63 | var builder = new WebHostBuilder().Configure(app => app 64 | .MaxConcurrentRequests(maxConcurrentRequests) 65 | .Use(async (context, _) => 66 | { 67 | byte[] bytes = Enumerable.Repeat((byte)0x1, 2).ToArray(); 68 | context.Response.StatusCode = (int)HttpStatusCode.OK; 69 | context.Response.ContentLength = bytes.Length * 2; 70 | context.Response.ContentType = "application/octet-stream"; 71 | 72 | // writing the response body flushes the headers 73 | await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); 74 | await waitHandle; 75 | await context.Response.Body.WriteAsync(bytes, 0, bytes.Length); 76 | })); 77 | 78 | var server = new TestServer(builder); 79 | return server.CreateClient(); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/MaxQueryStringTests.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware.Tests 2 | { 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Extensions; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.TestHost; 10 | using Shouldly; 11 | using Xunit; 12 | 13 | public class MaxQueryStringTests 14 | { 15 | [Fact] 16 | public async Task When_max_queryString_length_is_10_then_a_request_with_9_should_be_served() 17 | { 18 | HttpClient client = CreateClient(10); 19 | 20 | HttpResponseMessage response = await client.GetAsync("http://example.com?q=1234567"); 21 | 22 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 23 | } 24 | 25 | [Fact] 26 | public async Task When_max_queryString_length_is_10_then_a_request_with_11_should_be_rejected() 27 | { 28 | HttpClient client = CreateClient(10); 29 | 30 | HttpResponseMessage response = await client.GetAsync("http://example.com?q=123456789"); 31 | 32 | response.StatusCode.ShouldBe(HttpStatusCode.RequestUriTooLong); 33 | } 34 | 35 | [Fact] 36 | public async Task When_max_queryString_length_is_10_then_a_request_with_escaped_length_greater_than_10_but_unescaped_lower_or_equal_than_10_should_be_served() 37 | { 38 | HttpClient client = CreateClient(10); 39 | 40 | HttpResponseMessage response = await client.GetAsync("http://example.com?q=%48%49%50%51%52%53%54"); 41 | 42 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 43 | } 44 | 45 | [Fact] 46 | public async Task When_queryString_exceeds_max_length_then_request_should_be_rejected() 47 | { 48 | HttpClient client = CreateClient(5); 49 | 50 | HttpResponseMessage response = await client.GetAsync("http://example.com?q=123456"); 51 | 52 | response.StatusCode.ShouldBe(HttpStatusCode.RequestUriTooLong); 53 | } 54 | 55 | private static HttpClient CreateClient(int length) 56 | { 57 | var builder = new WebHostBuilder().Configure(app => app 58 | .MaxQueryStringLength(length) 59 | .Use((context, next) => 60 | { 61 | if (context.Response.IsSuccessStatusCode()) 62 | { 63 | context.Response.StatusCode = (int)HttpStatusCode.OK; 64 | } 65 | return Task.FromResult(0); 66 | })); 67 | 68 | var server = new TestServer(builder); 69 | return server.CreateClient(); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/MaxRequestContentLengthTests.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware.Tests 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Net.Http; 8 | using System.Threading.Tasks; 9 | using Extensions; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Hosting; 12 | using Microsoft.AspNetCore.TestHost; 13 | using Shouldly; 14 | using Xunit; 15 | 16 | public class MaxRequestContentLengthTests 17 | { 18 | [Fact] 19 | public async Task When_max_contentLength_is_20_and_a_get_request_is_coming_it_should_be_served() 20 | { 21 | var client = CreateHttpClient(20); 22 | var response = await client.GetAsync("/"); 23 | 24 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 25 | } 26 | 27 | 28 | [Fact] 29 | public async Task When_max_contentLength_is_20_and_a_get_request_with_contentLength_header_is_coming_then_the_header_should_be_ignored_and_the_request_served() 30 | { 31 | var client = CreateHttpClient(20); 32 | client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Length", 10.ToString()); 33 | var response = await client.GetAsync("/"); 34 | 35 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 36 | } 37 | 38 | [Fact] 39 | public async Task When_max_contentLength_is_20_and_a_head_request_then_it_should_be_served() 40 | { 41 | var client = CreateHttpClient(20); 42 | var request = new HttpRequestMessage(HttpMethod.Head, "/"); 43 | var response = await client.SendAsync(request); 44 | 45 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 46 | } 47 | 48 | [Fact] 49 | public async Task When_max_contentLength_is_20_and_a_post_with_contentLength_15_then_it_should_be_served() 50 | { 51 | var client = CreateHttpClient(20); 52 | var request = new HttpRequestMessage(HttpMethod.Post, "/"); 53 | AddContent(request, 5); 54 | var response = await client.SendAsync(request); 55 | 56 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 57 | } 58 | 59 | [Fact] 60 | public async Task When_max_contentLength_is_20_and_a_post_with_contentLength_21_then_it_should_be_rejected() 61 | { 62 | var client = CreateHttpClient(20); 63 | var request = new HttpRequestMessage(HttpMethod.Post, "/"); 64 | AddContent(request, 21); 65 | var response = await client.SendAsync(request); 66 | 67 | response.StatusCode.ShouldBe(HttpStatusCode.RequestEntityTooLarge); 68 | } 69 | 70 | [Fact] 71 | public async Task When_max_contentLength_is_20_and_a_put_with_contentLength_15_then_it_should_be_served() 72 | { 73 | var client = CreateHttpClient(20); 74 | var request = new HttpRequestMessage(HttpMethod.Put, "/"); 75 | AddContent(request, 15); 76 | var response = await client.SendAsync(request); 77 | 78 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 79 | } 80 | 81 | [Fact] 82 | public async Task When_max_contentLength_is_20_and_a_put_with_contentLength_21_then_it_should_be_rejected() 83 | { 84 | var client = CreateHttpClient(20); 85 | var request = new HttpRequestMessage(HttpMethod.Put, "/"); 86 | AddContent(request, 21); 87 | var response = await client.SendAsync(request); 88 | 89 | response.StatusCode.ShouldBe(HttpStatusCode.RequestEntityTooLarge); 90 | } 91 | 92 | [Fact] 93 | public async Task When_max_contentLength_is_20_and_a_patch_with_contentLength_15_then_it_should_be_served() 94 | { 95 | var client = CreateHttpClient(20); 96 | var request = new HttpRequestMessage(new HttpMethod("PATCH"), "/"); 97 | AddContent(request, 5); 98 | var response = await client.SendAsync(request); 99 | 100 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 101 | } 102 | 103 | [Fact] 104 | public async Task When_max_contentLength_is_20_and_a_patch_with_contentLength_21_then_it_should_be_rejected() 105 | { 106 | var client = CreateHttpClient(20); 107 | var request = new HttpRequestMessage(new HttpMethod("PATCH"), "/"); 108 | AddContent(request, 21); 109 | var response = await client.SendAsync(request); 110 | 111 | response.StatusCode.ShouldBe(HttpStatusCode.RequestEntityTooLarge); 112 | } 113 | 114 | [Fact] 115 | public async Task When_max_contentLength_is_20_and_the_contentLength_header_in_a_post_is_absent_then_it_should_be_rejected() 116 | { 117 | var client = CreateHttpClient(20); 118 | var response = await client.PostAsync("/", null); 119 | 120 | response.StatusCode.ShouldBe(HttpStatusCode.LengthRequired); 121 | } 122 | 123 | [Fact] 124 | public async Task When_max_contentLength_is_20_and_the_contentLength_header_is_not_a_valid_number_then_it_should_be_rejected() 125 | { 126 | var client = CreateHttpClient(20); 127 | var request = new HttpRequestMessage(HttpMethod.Post, "/") { Content = new NoLengthContent() }; 128 | request.Content.Headers.TryAddWithoutValidation("Content-Length", "NotAValidNumber"); 129 | var response = await client.SendAsync(request); 130 | 131 | response.StatusCode.ShouldBe(HttpStatusCode.LengthRequired); 132 | } 133 | 134 | [Fact] 135 | public async Task When_max_contentLength_is_2_and_the_request_is_chunked_and_longer_it_should_be_rejected() 136 | { 137 | var client = CreateHttpClient(2); 138 | var request = new HttpRequestMessage(HttpMethod.Post, "/") 139 | { 140 | Content = new StringContent("4\r\nWiki\r\n5\r\npedia\r\ne\r\nin\r\n\r\nchunks.\r\n0\r\n\r\n") 141 | }; 142 | request.Headers.TransferEncodingChunked = true; 143 | var response = await client.SendAsync(request); 144 | 145 | response.StatusCode.ShouldBe(HttpStatusCode.RequestEntityTooLarge); 146 | } 147 | 148 | private static HttpClient CreateHttpClient(int maxContentLength) => CreateHttpClient(_ => maxContentLength); 149 | 150 | private static HttpClient CreateHttpClient(Func getMaxContentLength) 151 | { 152 | var builder = new WebHostBuilder().Configure(app => app 153 | .MaxRequestContentLength(getMaxContentLength) 154 | .Use(async (context, _) => 155 | { 156 | await new StreamReader(context.Request.Body).ReadToEndAsync(); 157 | await new StreamWriter(context.Response.Body).WriteAsync("Lorem ipsum"); 158 | if (context.Response.IsSuccessStatusCode()) 159 | { 160 | context.Response.StatusCode = (int)HttpStatusCode.OK; 161 | } 162 | })); 163 | 164 | var server = new TestServer(builder); 165 | return server.CreateClient(); 166 | } 167 | 168 | private static void AddContent(HttpRequestMessage request, int contentLength) 169 | { 170 | request.Content = new ByteArrayContent(Enumerable.Repeat(byte.MinValue, contentLength).ToArray()); 171 | request.Content.Headers.ContentLength = contentLength; 172 | } 173 | 174 | private class NoLengthContent : HttpContent 175 | { 176 | protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => Task.FromResult(0); 177 | 178 | protected override bool TryComputeLength(out long length) 179 | { 180 | length = -1; 181 | return false; 182 | } 183 | } 184 | } 185 | } -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/MaxUrlLengthTests.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware.Tests 2 | { 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | using Extensions; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.TestHost; 10 | using Shouldly; 11 | using Xunit; 12 | 13 | public class MaxUrlLengthTests 14 | { 15 | [Fact] 16 | public async Task When_max_urlLength_is_20_and_a_url_with_length_18_should_be_served() 17 | { 18 | var client = CreateClient(20); 19 | var response = await client.GetAsync("http://example.com"); 20 | 21 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 22 | } 23 | 24 | [Fact] 25 | public async Task When_max_urlLength_is_20_and_a_url_with_length_39_is_coming_it_should_be_rejected() 26 | { 27 | var client = CreateClient(20); 28 | var response = await client.GetAsync("http://example.com/example/example.html"); 29 | 30 | response.StatusCode.ShouldBe(HttpStatusCode.RequestUriTooLong); 31 | } 32 | 33 | [Fact] 34 | public async Task When_max_urlLength_is_30_and_a_url_with_escaped_length_42_but_unescaped_28_it_should_be_served() 35 | { 36 | var client = CreateClient(30); 37 | var response = await client.GetAsync("http://example.com?q=%48%49%50%51%52%53%54"); 38 | 39 | response.StatusCode.ShouldBe(HttpStatusCode.OK); 40 | } 41 | 42 | private static HttpClient CreateClient(int length) 43 | { 44 | var builder = new WebHostBuilder().Configure(app => app 45 | .MaxQueryStringLength(length) 46 | .MaxUrlLength(length) 47 | .Use((context, next) => 48 | { 49 | if (context.Response.IsSuccessStatusCode()) 50 | { 51 | context.Response.StatusCode = (int)HttpStatusCode.OK; 52 | } 53 | return Task.FromResult(0); 54 | })); 55 | 56 | var server = new TestServer(builder); 57 | return server.CreateClient(); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/MinResponseDelayMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace LimitsMiddleware.Tests 3 | { 4 | using System; 5 | using System.Diagnostics; 6 | using System.Net; 7 | using System.Net.Http; 8 | using System.Threading.Tasks; 9 | using Extensions; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Hosting; 12 | using Microsoft.AspNetCore.TestHost; 13 | using Shouldly; 14 | using Xunit; 15 | 16 | public class MinResponseDelayMiddlewareTests 17 | { 18 | [Fact] 19 | public async Task When_response_delay_is_applied_then_time_to_receive_data_should_be_longer() 20 | { 21 | var stopwatch = new Stopwatch(); 22 | 23 | using (var client = CreateHttpClient(() => TimeSpan.Zero)) 24 | { 25 | stopwatch.Start(); 26 | 27 | await client.GetAsync("http://example.com"); 28 | 29 | stopwatch.Stop(); 30 | } 31 | 32 | TimeSpan noLimitTimespan = stopwatch.Elapsed; 33 | 34 | using (var client = CreateHttpClient(() => TimeSpan.FromMilliseconds(10))) 35 | { 36 | stopwatch.Start(); 37 | 38 | await client.GetAsync("http://example.com"); 39 | 40 | stopwatch.Stop(); 41 | } 42 | 43 | var limitTimespan = stopwatch.Elapsed; 44 | 45 | limitTimespan.ShouldBeGreaterThan(noLimitTimespan); 46 | } 47 | 48 | private static HttpClient CreateHttpClient(Func getMinDelay) 49 | { 50 | var builder = new WebHostBuilder().Configure(app => app 51 | .MinResponseDelay(getMinDelay) 52 | .Use((context, _) => 53 | { 54 | context.Response.StatusCode = (int)HttpStatusCode.OK; 55 | 56 | return Task.FromResult(0); 57 | })); 58 | 59 | var server = new TestServer(builder); 60 | return server.CreateClient(); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("LimitsMiddleware.Tests")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("1e42d5f7-31eb-4c2b-afd6-841d2024e636")] 20 | -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/RateLimiters/FixedTokenBucketTests.cs: -------------------------------------------------------------------------------- 1 | namespace LimitsMiddleware.Tests.RateLimiters 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using LimitsMiddleware.RateLimiters; 6 | using Shouldly; 7 | using Xunit; 8 | 9 | public class FixedTokenBucketTests 10 | { 11 | private const int MAX_TOKENS = 10; 12 | private const long REFILL_INTERVAL = 10; 13 | private const int N_LESS_THAN_MAX = 2; 14 | private const int N_GREATER_THAN_MAX = 12; 15 | private const int CUMULATIVE = 2; 16 | private readonly FixedTokenBucket _bucket; 17 | private GetUtcNow _getUtcNow = () => SystemClock.GetUtcNow(); 18 | 19 | public FixedTokenBucketTests() 20 | { 21 | _bucket = new FixedTokenBucket(() => MAX_TOKENS, () => _getUtcNow()); 22 | } 23 | 24 | [Fact] 25 | public void ShouldThrottle_WhenCalledWithNTokensLessThanMax_ReturnsFalse() 26 | { 27 | TimeSpan waitTime; 28 | bool shouldThrottle = _bucket.ShouldThrottle(N_LESS_THAN_MAX, out waitTime); 29 | 30 | shouldThrottle.ShouldBeFalse(); 31 | _bucket.CurrentTokenCount.ShouldBe(MAX_TOKENS - N_LESS_THAN_MAX); 32 | } 33 | 34 | [Fact] 35 | public void ShouldThrottle_WhenCalledCumulativeNTimesIsLessThanMaxTokens_ReturnsFalse() 36 | { 37 | for (int i = 0; i < CUMULATIVE; i++) 38 | { 39 | _bucket.ShouldThrottle(N_LESS_THAN_MAX).ShouldBeFalse(); 40 | } 41 | 42 | long tokens = _bucket.CurrentTokenCount; 43 | 44 | tokens.ShouldBe(MAX_TOKENS - (CUMULATIVE*N_LESS_THAN_MAX)); 45 | } 46 | 47 | [Fact] 48 | public void ShouldThrottle_WhenCalledCumulativeNTimesIsGreaterThanMaxTokens_ReturnsTrue() 49 | { 50 | for (int i = 0; i < CUMULATIVE; i++) 51 | { 52 | _bucket.ShouldThrottle(N_GREATER_THAN_MAX).ShouldBeTrue(); 53 | } 54 | 55 | long tokens = _bucket.CurrentTokenCount; 56 | 57 | tokens.ShouldBe(MAX_TOKENS); 58 | } 59 | 60 | [Fact] 61 | public void ShouldThrottle_WhenCalledWithNLessThanMaxSleepNLessThanMax_ReturnsFalse() 62 | { 63 | _getUtcNow = () => new DateTime(2014, 2, 27, 0, 0, 0, DateTimeKind.Utc); 64 | var virtualNow = _getUtcNow(); 65 | 66 | bool before = _bucket.ShouldThrottle(N_LESS_THAN_MAX); 67 | long tokensBefore = _bucket.CurrentTokenCount; 68 | before.ShouldBeFalse(); 69 | tokensBefore.ShouldBe(MAX_TOKENS - N_LESS_THAN_MAX); 70 | 71 | _getUtcNow = () => virtualNow.AddSeconds(REFILL_INTERVAL); 72 | 73 | bool after = _bucket.ShouldThrottle(N_LESS_THAN_MAX); 74 | long tokensAfter = _bucket.CurrentTokenCount; 75 | 76 | after.ShouldBeFalse(); 77 | tokensAfter.ShouldBe(MAX_TOKENS - N_LESS_THAN_MAX); 78 | } 79 | 80 | [Fact] 81 | public void ShouldThrottle_WhenCalledWithNGreaterThanMaxSleepNGreaterThanMax_ReturnsTrue() 82 | { 83 | _getUtcNow = () => new DateTime(2014, 2, 27, 0, 0, 0, DateTimeKind.Utc); 84 | var virtualNow = _getUtcNow(); 85 | 86 | bool before = _bucket.ShouldThrottle(N_GREATER_THAN_MAX); 87 | long tokensBefore = _bucket.CurrentTokenCount; 88 | 89 | before.ShouldBeTrue(); 90 | tokensBefore.ShouldBe(MAX_TOKENS); 91 | 92 | _getUtcNow = () => virtualNow.AddSeconds(REFILL_INTERVAL); 93 | 94 | bool after = _bucket.ShouldThrottle(N_GREATER_THAN_MAX); 95 | long tokensAfter = _bucket.CurrentTokenCount; 96 | after.ShouldBeTrue(); 97 | tokensAfter.ShouldBe(MAX_TOKENS); 98 | } 99 | 100 | [Fact] 101 | public void ShouldThrottle_WhenCalledWithNLessThanMaxSleepCumulativeNLessThanMax() 102 | { 103 | _getUtcNow = () => new DateTime(2014, 2, 27, 0, 0, 0, DateTimeKind.Utc); 104 | var virtualNow = _getUtcNow(); 105 | 106 | long sum = 0; 107 | for (int i = 0; i < CUMULATIVE; i++) 108 | { 109 | _bucket.ShouldThrottle(N_LESS_THAN_MAX).ShouldBeFalse(); 110 | sum += N_LESS_THAN_MAX; 111 | } 112 | long tokensBefore = _bucket.CurrentTokenCount; 113 | tokensBefore.ShouldBe(MAX_TOKENS - sum); 114 | 115 | _getUtcNow = () => virtualNow.AddSeconds(REFILL_INTERVAL); 116 | 117 | for (int i = 0; i < CUMULATIVE; i++) 118 | { 119 | _bucket.ShouldThrottle(N_LESS_THAN_MAX).ShouldBeFalse(); 120 | } 121 | long tokensAfter = _bucket.CurrentTokenCount; 122 | tokensAfter.ShouldBe(MAX_TOKENS - sum); 123 | } 124 | 125 | [Fact] 126 | public void ShouldThrottle_WhenCalledWithCumulativeNLessThanMaxSleepCumulativeNGreaterThanMax() 127 | { 128 | _getUtcNow = () => new DateTime(2014, 2, 27, 0, 0, 0, DateTimeKind.Utc); 129 | var virtualNow = _getUtcNow(); 130 | 131 | long sum = 0; 132 | for (int i = 0; i < CUMULATIVE; i++) 133 | { 134 | _bucket.ShouldThrottle(N_LESS_THAN_MAX).ShouldBeFalse(); 135 | sum += N_LESS_THAN_MAX; 136 | } 137 | long tokensBefore = _bucket.CurrentTokenCount; 138 | tokensBefore.ShouldBe(MAX_TOKENS - sum); 139 | 140 | _getUtcNow = () => virtualNow.AddSeconds(REFILL_INTERVAL); 141 | 142 | for (int i = 0; i < 3*CUMULATIVE; i++) 143 | { 144 | _bucket.ShouldThrottle(N_LESS_THAN_MAX); 145 | } 146 | 147 | bool after = _bucket.ShouldThrottle(N_LESS_THAN_MAX); 148 | long tokensAfter = _bucket.CurrentTokenCount; 149 | 150 | after.ShouldBeTrue(); 151 | tokensAfter.ShouldBeLessThan(N_LESS_THAN_MAX); 152 | } 153 | 154 | [Fact] 155 | public void ShouldThrottle_WhenCalledWithCumulativeNGreaterThanMaxSleepCumulativeNLessThanMax() 156 | { 157 | _getUtcNow = () => new DateTime(2014, 2, 27, 0, 0, 0, DateTimeKind.Utc); 158 | var virtualNow = _getUtcNow(); 159 | 160 | for (int i = 0; i < 3*CUMULATIVE; i++) 161 | { 162 | _bucket.ShouldThrottle(N_LESS_THAN_MAX); 163 | } 164 | 165 | bool before = _bucket.ShouldThrottle(N_LESS_THAN_MAX); 166 | long tokensBefore = _bucket.CurrentTokenCount; 167 | 168 | before.ShouldBeTrue(); 169 | tokensBefore.ShouldBeLessThan(N_LESS_THAN_MAX); 170 | 171 | _getUtcNow = () => virtualNow.AddSeconds(REFILL_INTERVAL); 172 | 173 | long sum = 0; 174 | for (int i = 0; i < CUMULATIVE; i++) 175 | { 176 | _bucket.ShouldThrottle(N_LESS_THAN_MAX).ShouldBeFalse(); 177 | sum += N_LESS_THAN_MAX; 178 | } 179 | 180 | long tokensAfter = _bucket.CurrentTokenCount; 181 | tokensAfter.ShouldBe(MAX_TOKENS - sum); 182 | } 183 | 184 | [Fact] 185 | public void ShouldThrottle_WhenCalledWithCumulativeNGreaterThanMaxSleepCumulativeNGreaterThanMax() 186 | { 187 | _getUtcNow = () => new DateTime(2014, 2, 27, 0, 0, 0, DateTimeKind.Utc); 188 | var virtualNow = _getUtcNow(); 189 | 190 | for (int i = 0; i < 3*CUMULATIVE; i++) 191 | { 192 | _bucket.ShouldThrottle(N_LESS_THAN_MAX); 193 | } 194 | 195 | bool before = _bucket.ShouldThrottle(N_LESS_THAN_MAX); 196 | long tokensBefore = _bucket.CurrentTokenCount; 197 | 198 | before.ShouldBeTrue(); 199 | tokensBefore.ShouldBeLessThan(N_LESS_THAN_MAX); 200 | 201 | _getUtcNow = () => virtualNow.AddSeconds(REFILL_INTERVAL); 202 | 203 | for (int i = 0; i < 3*CUMULATIVE; i++) 204 | { 205 | _bucket.ShouldThrottle(N_LESS_THAN_MAX); 206 | } 207 | bool after = _bucket.ShouldThrottle(N_LESS_THAN_MAX); 208 | long tokensAfter = _bucket.CurrentTokenCount; 209 | 210 | after.ShouldBeTrue(); 211 | tokensAfter.ShouldBeLessThan(N_LESS_THAN_MAX); 212 | } 213 | 214 | [Fact] 215 | public async Task ShouldThrottle_WhenThread1NLessThanMaxAndThread2NLessThanMax() 216 | { 217 | var task1 = Task.Run(() => 218 | { 219 | bool throttle = _bucket.ShouldThrottle(N_LESS_THAN_MAX); 220 | throttle.ShouldBeFalse(); 221 | }); 222 | 223 | var task2 = Task.Run(() => 224 | { 225 | bool throttle = _bucket.ShouldThrottle(N_LESS_THAN_MAX); 226 | throttle.ShouldBeFalse(); 227 | }); 228 | 229 | await Task.WhenAll(task1, task2); 230 | 231 | _bucket.CurrentTokenCount.ShouldBe(MAX_TOKENS - 2*N_LESS_THAN_MAX); 232 | } 233 | 234 | [Fact] 235 | public async Task ShouldThrottle_Thread1NGreaterThanMaxAndThread2NGreaterThanMax() 236 | { 237 | bool shouldThrottle = _bucket.ShouldThrottle(N_GREATER_THAN_MAX); 238 | shouldThrottle.ShouldBeTrue(); 239 | 240 | var task1 = Task.Run(() => 241 | { 242 | bool throttle = _bucket.ShouldThrottle(N_GREATER_THAN_MAX); 243 | throttle.ShouldBeTrue(); 244 | }); 245 | 246 | var task2 = Task.Run(() => 247 | { 248 | bool throttle = _bucket.ShouldThrottle(N_GREATER_THAN_MAX); 249 | throttle.ShouldBeTrue(); 250 | }); 251 | 252 | await Task.WhenAll(task1, task2); 253 | 254 | _bucket.CurrentTokenCount.ShouldBe(MAX_TOKENS); 255 | } 256 | } 257 | } -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "testRunner": "xunit", 4 | 5 | "buildOptions": { 6 | "define": [ "LIBLOG_PORTABLE" ], 7 | "copyToOutput": { 8 | "include": [ "Files/*.*", "xunit.runner.json" ] 9 | } 10 | }, 11 | 12 | "dependencies": { 13 | "coveralls.io": "1.3.4", 14 | "dotnet-test-xunit": "2.2.0-preview2-build1029", 15 | "LimitsMiddleware.AspNetCore": { "target": "project" }, 16 | "Shouldly": "2.8.2", 17 | "OpenCover": "4.6.519", 18 | "xunit": "2.2.0-beta2-build3300", 19 | "Microsoft.AspNetCore.StaticFiles": "1.0.0", 20 | "Microsoft.AspNetCore.TestHost": "1.0.0", 21 | "System.Reflection": "4.1.0" 22 | }, 23 | 24 | "frameworks": { 25 | "netcoreapp1.0": { 26 | "imports": [ "dotnet54" ], 27 | "dependencies": { 28 | "Microsoft.NETCore.App": { 29 | "type": "platform", 30 | "version": "1.0.1" 31 | } 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/LimitsMiddleware.AspNetCore.Tests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "diagnosticMessages": true, 3 | "parallelizeTestCollections": false 4 | } --------------------------------------------------------------------------------