├── .gitattributes ├── .gitignore ├── AppvNext.Throttlebird.sln ├── AppvNext.Throttlebird ├── AppvNext.Throttlebird.csproj ├── Extensions.cs ├── Properties │ └── AssemblyInfo.cs ├── Throttlebird.nuspec ├── Throttlebird.snk ├── Throttling │ ├── IThrottleStore.cs │ ├── InMemoryThrottleStore.cs │ ├── ThrottleEntry.cs │ └── ThrottlingHandler.cs └── packages.config ├── GitVersionConfig.yaml ├── LICENSE.txt ├── README.md ├── appveyor.yml ├── build.bat ├── build.cake └── build.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.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 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | # DNX 42 | project.lock.json 43 | artifacts/ 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding add-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | ## TODO: Comment the next line if you want to checkin your 137 | ## web deploy settings but do note that will include unencrypted 138 | ## passwords 139 | #*.pubxml 140 | 141 | *.publishproj 142 | 143 | # NuGet Packages 144 | *.nupkg 145 | # The packages folder can be ignored because of Package Restore 146 | **/packages/* 147 | # except build/, which is used as an MSBuild target. 148 | !**/packages/build/ 149 | # Uncomment if necessary however generally it will be regenerated when needed 150 | #!**/packages/repositories.config 151 | 152 | # Windows Azure Build Output 153 | csx/ 154 | *.build.csdef 155 | 156 | # Windows Store app package directory 157 | AppPackages/ 158 | 159 | # Visual Studio cache files 160 | # files ending in .cache can be ignored 161 | *.[Cc]ache 162 | # but keep track of directories ending in .cache 163 | !*.[Cc]ache/ 164 | 165 | # Others 166 | ClientBin/ 167 | [Ss]tyle[Cc]op.* 168 | ~$* 169 | *~ 170 | *.dbmdl 171 | *.dbproj.schemaview 172 | *.pfx 173 | *.publishsettings 174 | node_modules/ 175 | orleans.codegen.cs 176 | 177 | # RIA/Silverlight projects 178 | Generated_Code/ 179 | 180 | # Backup & report files from converting an old project file 181 | # to a newer Visual Studio version. Backup files are not needed, 182 | # because we have git ;-) 183 | _UpgradeReport_Files/ 184 | Backup*/ 185 | UpgradeLog*.XML 186 | UpgradeLog*.htm 187 | 188 | # SQL Server files 189 | *.mdf 190 | *.ldf 191 | 192 | # Business Intelligence projects 193 | *.rdl.data 194 | *.bim.layout 195 | *.bim_*.settings 196 | 197 | # Microsoft Fakes 198 | FakesAssemblies/ 199 | 200 | # Node.js Tools for Visual Studio 201 | .ntvs_analysis.dat 202 | 203 | # Visual Studio 6 build log 204 | *.plg 205 | 206 | # Visual Studio 6 workspace options file 207 | *.opt 208 | 209 | # LightSwitch generated files 210 | GeneratedArtifacts/ 211 | _Pvt_Extensions/ 212 | ModelManifest.xml 213 | -------------------------------------------------------------------------------- /AppvNext.Throttlebird.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.23107.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AppvNext.Throttlebird", "AppvNext.Throttlebird\AppvNext.Throttlebird.csproj", "{1E86B5C1-EEA2-429C-82C7-7DBD5F978A01}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {1E86B5C1-EEA2-429C-82C7-7DBD5F978A01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {1E86B5C1-EEA2-429C-82C7-7DBD5F978A01}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {1E86B5C1-EEA2-429C-82C7-7DBD5F978A01}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {1E86B5C1-EEA2-429C-82C7-7DBD5F978A01}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /AppvNext.Throttlebird/AppvNext.Throttlebird.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {1E86B5C1-EEA2-429C-82C7-7DBD5F978A01} 8 | Library 9 | Properties 10 | AppvNext.Throttlebird 11 | AppvNext.Throttlebird 12 | v4.5.2 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | false 34 | 35 | 36 | Throttlebird.snk 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ..\packages\System.Net.Http.Formatting.Extension.5.2.3.0\lib\System.Net.Http.Extensions.dll 45 | True 46 | 47 | 48 | ..\packages\System.Net.Http.Formatting.Extension.5.2.3.0\lib\System.Net.Http.Formatting.dll 49 | True 50 | 51 | 52 | ..\packages\System.Net.Http.Formatting.Extension.5.2.3.0\lib\System.Net.Http.Primitives.dll 53 | True 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 83 | -------------------------------------------------------------------------------- /AppvNext.Throttlebird/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Net.Http; 6 | 7 | namespace AppvNext.Throttlebird.Extensions 8 | { 9 | public static class HttpRequestMessageExtensions 10 | { 11 | private const string HttpContext = "MS_HttpContext"; 12 | private const string RemoteEndpointMessage = "System.ServiceModel.Channels.RemoteEndpointMessageProperty"; 13 | private const string OwinContext = "MS_OwinContext"; 14 | 15 | public static bool IsLocal(this HttpRequestMessage request) 16 | { 17 | var localFlag = request.Properties["MS_IsLocal"] as Lazy; 18 | return localFlag != null && localFlag.Value; 19 | } 20 | 21 | public static string GetClientIpAddress(this HttpRequestMessage request) 22 | { 23 | //Web-hosting 24 | if (request.Properties.ContainsKey(HttpContext)) 25 | { 26 | dynamic ctx = request.Properties[HttpContext]; 27 | if (ctx != null) 28 | { 29 | return ctx.Request.UserHostAddress; 30 | } 31 | } 32 | //Self-hosting 33 | if (request.Properties.ContainsKey(RemoteEndpointMessage)) 34 | { 35 | dynamic remoteEndpoint = request.Properties[RemoteEndpointMessage]; 36 | if (remoteEndpoint != null) 37 | { 38 | return remoteEndpoint.Address; 39 | } 40 | } 41 | //Owin-hosting 42 | if (request.Properties.ContainsKey(OwinContext)) 43 | { 44 | dynamic ctx = request.Properties[OwinContext]; 45 | if (ctx != null) 46 | { 47 | return ctx.Request.RemoteIpAddress; 48 | } 49 | } 50 | return null; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /AppvNext.Throttlebird/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("AppvNext.Throttlebird")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("App vNext")] 12 | [assembly: AssemblyProduct("AppvNext.Throttlebird")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("1e86b5c1-eea2-429c-82c7-7dbd5f978a01")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.1.0")] 36 | [assembly: AssemblyFileVersion("1.0.1.0")] 37 | -------------------------------------------------------------------------------- /AppvNext.Throttlebird/Throttlebird.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | App vNext 5 | Joel Hulen, App vNext 6 | 7 | Throttlebird is a simple Http request throttler to help limit the number of client requests within a given period of time. 8 | 9 | en-US 10 | https://raw.github.com/App-vNext/Throttlebird/master/LICENSE.txt 11 | https://github.com/App-vNext/Throttlebird 12 | ASP.NET MVC HTTP Tools 13 | Copyright 2015, App vNext 14 | 15 | 1.0.0 16 | --------------------- 17 | - Initial release 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /AppvNext.Throttlebird/Throttlebird.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/App-vNext/Throttlebird/a703363c9ef18dc0b0b41207527e51d06f08eae7/AppvNext.Throttlebird/Throttlebird.snk -------------------------------------------------------------------------------- /AppvNext.Throttlebird/Throttling/IThrottleStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace AppvNext.Throttlebird.Throttling 8 | { 9 | /// 10 | /// Interface for caching request throttling data. 11 | /// 12 | public interface IThrottleStore 13 | { 14 | bool TryGetValue(string key, out ThrottleEntry entry); 15 | void IncrementRequests(string key); 16 | void Rollover(string key); 17 | void Clear(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /AppvNext.Throttlebird/Throttling/InMemoryThrottleStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Web; 6 | 7 | namespace AppvNext.Throttlebird.Throttling 8 | { 9 | /// 10 | /// Creates an in-memory throttle cache to keep track of how many Http 11 | /// requests within a timespan each client is making. 12 | /// 13 | public class InMemoryThrottleStore : IThrottleStore 14 | { 15 | private readonly ConcurrentDictionary _throttleStore = new ConcurrentDictionary(); 16 | 17 | public bool TryGetValue(string key, out ThrottleEntry entry) 18 | { 19 | return _throttleStore.TryGetValue(key, out entry); 20 | } 21 | 22 | public void IncrementRequests(string key) 23 | { 24 | _throttleStore.AddOrUpdate(key, 25 | k => 26 | { 27 | return new ThrottleEntry() { Requests = 1 }; 28 | }, 29 | (k, e) => 30 | { 31 | e.Requests++; 32 | return e; 33 | }); 34 | } 35 | 36 | public void Rollover(string key) 37 | { 38 | ThrottleEntry dummy; 39 | _throttleStore.TryRemove(key, out dummy); 40 | } 41 | 42 | public void Clear() 43 | { 44 | _throttleStore.Clear(); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /AppvNext.Throttlebird/Throttling/ThrottleEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | 6 | namespace AppvNext.Throttlebird.Throttling 7 | { 8 | /// 9 | /// Creates an entry in our throttle store. 10 | /// 11 | public class ThrottleEntry 12 | { 13 | public DateTime PeriodStart { get; set; } 14 | public long Requests { get; set; } 15 | 16 | public ThrottleEntry() 17 | { 18 | PeriodStart = DateTime.UtcNow; 19 | Requests = 0; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /AppvNext.Throttlebird/Throttling/ThrottlingHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Web; 9 | using AppvNext.Throttlebird.Extensions; 10 | 11 | namespace AppvNext.Throttlebird.Throttling 12 | { 13 | public class ThrottlingHandler 14 | : DelegatingHandler 15 | { 16 | private readonly IThrottleStore _store; 17 | private readonly Func _maxRequestsForUserIdentifier; 18 | private readonly TimeSpan _period; 19 | private readonly string _message; 20 | 21 | public ThrottlingHandler(IThrottleStore store, Func maxRequestsForUserIdentifier, TimeSpan period) 22 | : this(store, maxRequestsForUserIdentifier, period, "The allowed number of requests has been exceeded.") 23 | { 24 | } 25 | 26 | public ThrottlingHandler(IThrottleStore store, Func maxRequestsForUserIdentifier, TimeSpan period, string message) 27 | { 28 | _store = store; 29 | _maxRequestsForUserIdentifier = maxRequestsForUserIdentifier; 30 | _period = period; 31 | _message = message; 32 | } 33 | 34 | protected virtual string GetUserIdentifier(HttpRequestMessage request) 35 | { 36 | return request.GetClientIpAddress(); 37 | } 38 | 39 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 40 | { 41 | var identifier = GetUserIdentifier(request); 42 | 43 | if (string.IsNullOrEmpty(identifier)) 44 | { 45 | return CreateResponse(request, HttpStatusCode.Forbidden, "Could not identify client."); 46 | } 47 | 48 | var maxRequests = _maxRequestsForUserIdentifier(identifier); 49 | 50 | ThrottleEntry entry = null; 51 | if (_store.TryGetValue(identifier, out entry)) 52 | { 53 | if (entry.PeriodStart + _period < DateTime.UtcNow) 54 | { 55 | _store.Rollover(identifier); 56 | } 57 | } 58 | _store.IncrementRequests(identifier); 59 | if (!_store.TryGetValue(identifier, out entry)) 60 | { 61 | return CreateResponse(request, HttpStatusCode.Forbidden, "Could not identify client."); 62 | } 63 | 64 | Task response = null; 65 | if (entry.Requests > maxRequests) 66 | { 67 | response = CreateResponse(request, HttpStatusCode.ServiceUnavailable, _message); 68 | } 69 | else 70 | { 71 | response = base.SendAsync(request, cancellationToken); 72 | } 73 | 74 | return response.ContinueWith(task => 75 | { 76 | var remaining = maxRequests - entry.Requests; 77 | if (remaining < 0) 78 | { 79 | remaining = 0; 80 | } 81 | 82 | var httpResponse = task.Result; 83 | httpResponse.Headers.Add("RateLimit-Limit", maxRequests.ToString()); 84 | httpResponse.Headers.Add("RateLimit-Remaining", remaining.ToString()); 85 | 86 | return httpResponse; 87 | }); 88 | } 89 | 90 | protected Task CreateResponse(HttpRequestMessage request, HttpStatusCode statusCode, string message) 91 | { 92 | var tsc = new TaskCompletionSource(); 93 | var response = request.CreateResponse(statusCode); 94 | response.ReasonPhrase = message; 95 | response.Content = new StringContent(message); 96 | tsc.SetResult(response); 97 | return tsc.Task; 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /AppvNext.Throttlebird/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /GitVersionConfig.yaml: -------------------------------------------------------------------------------- 1 | next-version: 1.0.1 -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | New BSD License 2 | = 3 | Copyright (c) 2015, App vNext 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither the name of the App vNext nor the 14 | names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ______ __ __ ____ ___ ______ ______ _ ___ ____ ____ ____ ___ 3 | | T| T T| \ / \ | T| T| T / _]| \ l j| \ | \ 4 | | || l || D )Y Y| || || | / [_ | o ) | T | D )| \ 5 | l_j l_j| _ || / | O |l_j l_jl_j l_j| l___ Y _]| T | | | / | D Y 6 | | | | | || \ | | | | | | | T| [_ | O | | | | \ | | 7 | | | | | || . Yl ! | | | | | || T| | j l | . Y| | 8 | l__j l__j__jl__j\_j \___/ l__j l__j l_____jl_____jl_____j|____jl__j\_jl_____j 9 | 10 | ``` 11 | 12 | # Throttlebird 13 | Throttlebird is a simple http request throttler to help limit the number of client requests within a given period of time. 14 | 15 | [![NuGet version](https://badge.fury.io/nu/Throttlebird.svg)](https://badge.fury.io/nu/Throttlebird) [![Build status](https://ci.appveyor.com/api/projects/status/c2xv4a7fqmfml1qy?svg=true)](https://ci.appveyor.com/project/joelhulen/throttlebird) 16 | 17 | # Example 18 | In your WebApiConfig file under the App_Start folder of your ASP.NET MVC project, add the following code block within the Register method: 19 | 20 | ```csharp 21 | // Implement our custom throttling handler to limit API method calls. 22 | // Specify the throttle store, max number of allowed requests within specified timespan, 23 | // and message displayed in the error response when exceeded. 24 | config.MessageHandlers.Add(new ThrottlingHandler( 25 | new InMemoryThrottleStore(), 26 | id => 3, 27 | TimeSpan.FromSeconds(5), 28 | "You have exceeded the maximum number of allowed calls. Please wait until after the cooldown period to try again." 29 | )); 30 | ``` 31 | 32 | This works across all http requests on your site, limiting the number of requests to 3 every 5 seconds. To adjust, simply modify the *id* parameter to the number of requests, and the timespan to any valid TimeSpan value (FromSeconds, FromMinutes, etc.) to the values that work for you. 33 | 34 | # More to Come 35 | This is the the first iteration of this library. Future expansions will allow you to selectively apply throttling on specific controller actions through the use of attributes. 36 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | os: Visual Studio 2015 2 | 3 | # Build script 4 | build_script: 5 | - ps: .\build.ps1 6 | 7 | # Tests 8 | test: off 9 | 10 | artifacts: 11 | - path: artifacts\nuget-package\*.nupkg 12 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | PUSHD %~dp0 3 | PowerShell.exe -NoProfile -ExecutionPolicy Bypass -Command "& './build.ps1'" 4 | 5 | IF %errorlevel% neq 0 PAUSE 6 | 7 | -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // ARGUMENTS 3 | /////////////////////////////////////////////////////////////////////////////// 4 | 5 | var target = Argument("target", "Default"); 6 | var configuration = Argument("configuration", "Release"); 7 | 8 | ////////////////////////////////////////////////////////////////////// 9 | // EXTERNAL NUGET TOOLS 10 | ////////////////////////////////////////////////////////////////////// 11 | 12 | #Tool "xunit.runner.console" 13 | #Tool "GitVersion.CommandLine" 14 | #Tool "Brutal.Dev.StrongNameSigner" 15 | #Tool "NuSpec.ReferenceGenerator" 16 | 17 | ////////////////////////////////////////////////////////////////////// 18 | // EXTERNAL NUGET LIBRARIES 19 | ////////////////////////////////////////////////////////////////////// 20 | 21 | #addin "System.Text.Json" 22 | using System.Text.Json; 23 | 24 | /////////////////////////////////////////////////////////////////////////////// 25 | // GLOBAL VARIABLES 26 | /////////////////////////////////////////////////////////////////////////////// 27 | 28 | var projectName = "Throttlebird"; 29 | var keyName = "Throttlebird.snk"; 30 | 31 | var solutions = GetFiles("./**/*.sln"); 32 | var solutionPaths = solutions.Select(solution => solution.GetDirectory()); 33 | 34 | var srcDir = Directory("./AppvNext.Throttlebird"); 35 | var buildDir = Directory("./build"); 36 | var artifactsDir = Directory("./artifacts"); 37 | var testResultsDir = artifactsDir + Directory("test-results"); 38 | 39 | // NuGet 40 | var nuspecFilename = projectName + ".nuspec"; 41 | var nuspecSrcFile = srcDir + File(nuspecFilename); 42 | var nuspecDestFile = buildDir + File(nuspecFilename); 43 | var nupkgDestDir = artifactsDir + Directory("nuget-package"); 44 | var snkFile = srcDir + File(keyName); 45 | 46 | var projectToNugetFolderMap = new Dictionary() { 47 | { "Net45", new [] {"net45"} } 48 | }; 49 | 50 | // Gitversion 51 | var gitVersionPath = ToolsExePath("GitVersion.exe"); 52 | Dictionary gitVersionOutput; 53 | 54 | // StrongNameSigner 55 | var strongNameSignerPath = ToolsExePath("StrongNameSigner.Console.exe"); 56 | 57 | // NuSpec.ReferenceGenerator 58 | var refGenPath = ToolsExePath("RefGen.exe"); 59 | 60 | /////////////////////////////////////////////////////////////////////////////// 61 | // SETUP / TEARDOWN 62 | /////////////////////////////////////////////////////////////////////////////// 63 | 64 | Setup(() => 65 | { 66 | Information(""); 67 | Information(" ████████╗██╗ ██╗██████╗ ██████╗ ████████╗████████╗██╗ ███████╗██████╗ ██╗██████╗ ██████╗ "); 68 | Information(" ╚══██╔══╝██║ ██║██╔══██╗██╔═══██╗╚══██╔══╝╚══██╔══╝██║ ██╔════╝██╔══██╗██║██╔══██╗██╔══██╗"); 69 | Information(" ██║ ███████║██████╔╝██║ ██║ ██║ ██║ ██║ █████╗ ██████╔╝██║██████╔╝██║ ██║"); 70 | Information(" ██║ ██╔══██║██╔══██╗██║ ██║ ██║ ██║ ██║ ██╔══╝ ██╔══██╗██║██╔══██╗██║ ██║"); 71 | Information(" ██║ ██║ ██║██║ ██║╚██████╔╝ ██║ ██║ ███████╗███████╗██████╔╝██║██║ ██║██████╔╝"); 72 | Information(" ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝╚═════╝ ╚═╝╚═╝ ╚═╝╚═════╝ "); 73 | Information(""); 74 | }); 75 | 76 | Teardown(() => 77 | { 78 | Information("Finished running tasks."); 79 | }); 80 | 81 | ////////////////////////////////////////////////////////////////////// 82 | // PRIVATE TASKS 83 | ////////////////////////////////////////////////////////////////////// 84 | 85 | Task("__Clean") 86 | .Does(() => 87 | { 88 | CleanDirectories(new DirectoryPath[] { 89 | buildDir, 90 | artifactsDir, 91 | testResultsDir, 92 | nupkgDestDir 93 | }); 94 | 95 | foreach(var path in solutionPaths) 96 | { 97 | Information("Cleaning {0}", path); 98 | CleanDirectories(path + "/**/bin/" + configuration); 99 | CleanDirectories(path + "/**/obj/" + configuration); 100 | } 101 | }); 102 | 103 | Task("__RestoreNugetPackages") 104 | .Does(() => 105 | { 106 | foreach(var solution in solutions) 107 | { 108 | Information("Restoring NuGet Packages for {0}", solution); 109 | NuGetRestore(solution); 110 | } 111 | }); 112 | 113 | Task("__UpdateAssemblyVersionInformation") 114 | .Does(() => 115 | { 116 | var gitVersionSettings = new ProcessSettings() 117 | .SetRedirectStandardOutput(true); 118 | 119 | IEnumerable outputLines; 120 | StartProcess(gitVersionPath, gitVersionSettings, out outputLines); 121 | 122 | var output = string.Join("\n", outputLines); 123 | gitVersionOutput = new JsonParser().Parse>(output); 124 | 125 | Information("Updated GlobalAssemblyInfo"); 126 | Information("AssemblyVersion -> {0}", gitVersionOutput["AssemblySemVer"]); 127 | Information("AssemblyFileVersion -> {0}", gitVersionOutput["MajorMinorPatch"]); 128 | Information("AssemblyInformationalVersion -> {0}", gitVersionOutput["InformationalVersion"]); 129 | }); 130 | 131 | Task("__UpdateAppVeyorBuildNumber") 132 | .WithCriteria(() => AppVeyor.IsRunningOnAppVeyor) 133 | .Does(() => 134 | { 135 | var fullSemVer = gitVersionOutput["FullSemVer"].ToString(); 136 | AppVeyor.UpdateBuildVersion(fullSemVer); 137 | }); 138 | 139 | Task("__BuildSolutions") 140 | .Does(() => 141 | { 142 | foreach(var solution in solutions) 143 | { 144 | Information("Building {0}", solution); 145 | 146 | MSBuild(solution, settings => 147 | settings 148 | .SetConfiguration(configuration) 149 | .WithProperty("TreatWarningsAsErrors", "true") 150 | .UseToolVersion(MSBuildToolVersion.NET46) 151 | .SetVerbosity(Verbosity.Minimal) 152 | .SetNodeReuse(false)); 153 | } 154 | }); 155 | 156 | Task("__RunTests") 157 | .Does(() => 158 | { 159 | XUnit2("./src/**/bin/" + configuration + "/*.Specs.dll", new XUnit2Settings { 160 | OutputDirectory = testResultsDir, 161 | XmlReportV1 = true 162 | }); 163 | }); 164 | 165 | Task("__CopyOutputToNugetFolder") 166 | .Does(() => 167 | { 168 | foreach(var project in projectToNugetFolderMap.Keys) { 169 | // var sourceDir = srcDir + Directory(projectName + "." + project) + Directory("bin") + Directory(configuration); 170 | var sourceDir = srcDir + Directory("bin") + Directory(configuration); 171 | 172 | foreach(var targetFolder in projectToNugetFolderMap[project]) { 173 | var destDir = buildDir + Directory("lib") + Directory(targetFolder); 174 | 175 | Information("Copying {0} -> {1}.", sourceDir, destDir); 176 | CopyDirectory(sourceDir, destDir); 177 | } 178 | } 179 | 180 | CopyFile(nuspecSrcFile, nuspecDestFile); 181 | }); 182 | 183 | Task("__AddDotNetReferencesToNuspecFile") 184 | .Does(() => 185 | { 186 | // see: https://github.com/onovotny/ReferenceGenerator 187 | var pclProjectName = projectName + ".Pcl"; 188 | var pclDirectory = srcDir + Directory(pclProjectName); 189 | var projectFile = pclDirectory + File(pclProjectName + ".csproj"); 190 | var projectDll = pclDirectory + Directory("bin") + Directory(configuration) + File(projectName + ".dll"); 191 | 192 | var refGenSettings = new ProcessSettings() 193 | .WithArguments(args => args 194 | .AppendQuoted(".NETPortable,Version=v4.5,Profile=Profile259") 195 | .AppendQuoted("dotnet") 196 | .AppendQuoted(nuspecDestFile) 197 | .AppendQuoted(projectFile) 198 | .AppendQuoted(projectDll)); 199 | 200 | StartProcess(refGenPath, refGenSettings); 201 | }); 202 | 203 | Task("__CreateNugetPackage") 204 | .Does(() => 205 | { 206 | var nugetVersion = gitVersionOutput["NuGetVersion"].ToString(); 207 | var packageName = projectName; 208 | 209 | Information("Building {0}.{1}.nupkg", packageName, nugetVersion); 210 | 211 | var nuGetPackSettings = new NuGetPackSettings { 212 | Id = packageName, 213 | Title = packageName, 214 | Version = nugetVersion, 215 | OutputDirectory = nupkgDestDir 216 | }; 217 | 218 | NuGetPack(nuspecDestFile, nuGetPackSettings); 219 | }); 220 | 221 | Task("__StronglySignAssemblies") 222 | .Does(() => 223 | { 224 | //see: https://github.com/brutaldev/StrongNameSigner 225 | var strongNameSignerSettings = new ProcessSettings() 226 | .WithArguments(args => args 227 | .Append("-in") 228 | .AppendQuoted(buildDir) 229 | .Append("-k") 230 | .AppendQuoted(snkFile) 231 | .Append("-l") 232 | .AppendQuoted("Changes")); 233 | 234 | StartProcess(strongNameSignerPath, strongNameSignerSettings); 235 | }); 236 | 237 | Task("__CreateSignedNugetPackage") 238 | .Does(() => 239 | { 240 | var nugetVersion = gitVersionOutput["NuGetVersion"].ToString(); 241 | var packageName = projectName + "-Signed"; 242 | 243 | Information("Building {0}.{1}.nupkg", packageName, nugetVersion); 244 | 245 | var nuGetPackSettings = new NuGetPackSettings { 246 | Id = packageName, 247 | Title = packageName, 248 | Version = nugetVersion, 249 | OutputDirectory = nupkgDestDir 250 | }; 251 | 252 | NuGetPack(nuspecDestFile, nuGetPackSettings); 253 | }); 254 | 255 | ////////////////////////////////////////////////////////////////////// 256 | // BUILD TASKS 257 | ////////////////////////////////////////////////////////////////////// 258 | 259 | Task("Build") 260 | .IsDependentOn("__Clean") 261 | .IsDependentOn("__RestoreNugetPackages") 262 | .IsDependentOn("__UpdateAssemblyVersionInformation") 263 | .IsDependentOn("__UpdateAppVeyorBuildNumber") 264 | .IsDependentOn("__BuildSolutions") 265 | .IsDependentOn("__RunTests") 266 | .IsDependentOn("__CopyOutputToNugetFolder") 267 | .IsDependentOn("__AddDotNetReferencesToNuspecFile") 268 | .IsDependentOn("__CreateNugetPackage") 269 | .IsDependentOn("__StronglySignAssemblies") 270 | .IsDependentOn("__CreateSignedNugetPackage"); 271 | 272 | /////////////////////////////////////////////////////////////////////////////// 273 | // PRIMARY TARGETS 274 | /////////////////////////////////////////////////////////////////////////////// 275 | 276 | Task("Default") 277 | .IsDependentOn("Build"); 278 | 279 | /////////////////////////////////////////////////////////////////////////////// 280 | // EXECUTION 281 | /////////////////////////////////////////////////////////////////////////////// 282 | 283 | RunTarget(target); 284 | 285 | ////////////////////////////////////////////////////////////////////// 286 | // HELPER FUNCTIONS 287 | ////////////////////////////////////////////////////////////////////// 288 | 289 | string ToolsExePath(string exeFileName) { 290 | var exePath = System.IO.Directory.GetFiles(@".\Tools", exeFileName, SearchOption.AllDirectories).FirstOrDefault(); 291 | return exePath; 292 | } -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .SYNOPSIS 4 | This is a Powershell script to bootstrap a Cake build. 5 | 6 | .DESCRIPTION 7 | This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) 8 | and execute your Cake build script with the parameters you provide. 9 | 10 | .PARAMETER Script 11 | The build script to execute. 12 | .PARAMETER Target 13 | The build script target to run. 14 | .PARAMETER Configuration 15 | The build configuration to use. 16 | .PARAMETER Verbosity 17 | Specifies the amount of information to be displayed. 18 | .PARAMETER Experimental 19 | Tells Cake to use the latest Roslyn release. 20 | .PARAMETER WhatIf 21 | Performs a dry run of the build script. 22 | No tasks will be executed. 23 | .PARAMETER Mono 24 | Tells Cake to use the Mono scripting engine. 25 | 26 | .LINK 27 | http://cakebuild.net 28 | #> 29 | 30 | Param( 31 | [string]$Script = "build.cake", 32 | [string]$Target = "Default", 33 | [string]$Configuration = "Release", 34 | [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] 35 | [string]$Verbosity = "Verbose", 36 | [switch]$Experimental, 37 | [Alias("DryRun","Noop")] 38 | [switch]$WhatIf, 39 | [switch]$Mono, 40 | [switch]$SkipToolPackageRestore, 41 | [switch]$Verbose 42 | ) 43 | 44 | Write-Host "Preparing to run build script..." 45 | 46 | # Should we show verbose messages? 47 | if($Verbose.IsPresent) 48 | { 49 | $VerbosePreference = "continue" 50 | } 51 | 52 | $TOOLS_DIR = Join-Path $PSScriptRoot "tools" 53 | $NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" 54 | $CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" 55 | $PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" 56 | 57 | # Should we use mono? 58 | $UseMono = ""; 59 | if($Mono.IsPresent) { 60 | Write-Verbose -Message "Using the Mono based scripting engine." 61 | $UseMono = "-mono" 62 | } 63 | 64 | # Should we use the new Roslyn? 65 | $UseExperimental = ""; 66 | if($Experimental.IsPresent -and !($Mono.IsPresent)) { 67 | Write-Verbose -Message "Using experimental version of Roslyn." 68 | $UseExperimental = "-experimental" 69 | } 70 | 71 | # Is this a dry run? 72 | $UseDryRun = ""; 73 | if($WhatIf.IsPresent) { 74 | $UseDryRun = "-dryrun" 75 | } 76 | 77 | # Make sure tools folder exists 78 | if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { 79 | New-Item -Path $TOOLS_DIR -Type directory | out-null 80 | } 81 | 82 | # Try download NuGet.exe if not exists 83 | if (!(Test-Path $NUGET_EXE)) { 84 | Write-Verbose -Message "Downloading NuGet.exe..." 85 | Invoke-WebRequest -Uri https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile $NUGET_EXE 86 | } 87 | 88 | # Make sure NuGet exists where we expect it. 89 | if (!(Test-Path $NUGET_EXE)) { 90 | Throw "Could not find NuGet.exe" 91 | } 92 | 93 | # Save nuget.exe path to environment to be available to child processed 94 | $ENV:NUGET_EXE = $NUGET_EXE 95 | 96 | # Restore tools from NuGet? 97 | if(-Not $SkipToolPackageRestore.IsPresent) 98 | { 99 | # Restore tools from NuGet. 100 | Push-Location 101 | Set-Location $TOOLS_DIR 102 | 103 | Write-Verbose -Message "Restoring tools from NuGet..." 104 | 105 | # Restore packages 106 | if (Test-Path $PACKAGES_CONFIG) 107 | { 108 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion" 109 | Write-Verbose ($NuGetOutput | Out-String) 110 | } 111 | # Install just Cake if missing config 112 | else 113 | { 114 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install Cake -ExcludeVersion" 115 | Write-Verbose ($NuGetOutput | Out-String) 116 | } 117 | Pop-Location 118 | if ($LASTEXITCODE -ne 0) 119 | { 120 | exit $LASTEXITCODE 121 | } 122 | } 123 | 124 | # Make sure that Cake has been installed. 125 | if (!(Test-Path $CAKE_EXE)) { 126 | Throw "Could not find Cake.exe" 127 | } 128 | 129 | # Start Cake 130 | Write-Host "Running build script..." 131 | Invoke-Expression "$CAKE_EXE `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental" 132 | exit $LASTEXITCODE 133 | --------------------------------------------------------------------------------