Skip to content
This repository was archived by the owner on Nov 1, 2023. It is now read-only.

Commit a7686d3

Browse files
committed
Added IP range support
1 parent 17e08ed commit a7686d3

9 files changed

+206
-6
lines changed

WebApiThrottle.Demo/App_Start/WebApiConfig.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ public static void Register(HttpConfiguration config)
3737
IpThrottling = true,
3838
IpRules = new Dictionary<string, RateLimits>
3939
{
40-
{ "::1", new RateLimits { PerSecond = 2 } },
41-
{ "192.168.0.1", new RateLimits { PerMinute = 30, PerHour = 30*60, PerDay = 30*60*24 } }
40+
{ "::1/10", new RateLimits { PerSecond = 2 } },
41+
{ "192.168.2.1", new RateLimits { PerMinute = 30, PerHour = 30*60, PerDay = 30*60*24 } }
4242
},
4343
//white list the "::1" IP to disable throttling on localhost for Win8
44-
IpWhitelist = new List<string> { "127.0.0.1" },
44+
IpWhitelist = new List<string> { "127.0.0.1", "192.168.0.0/24" },
4545

4646
//scope to clients (if IP throttling is applied then the scope becomes a combination of IP and client key)
4747
ClientThrottling = true,

WebApiThrottle/IPAddressRange.cs

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Runtime.Serialization;
6+
using System.Text;
7+
using System.Text.RegularExpressions;
8+
using System.Threading.Tasks;
9+
10+
namespace WebApiThrottle
11+
{
12+
/// <summary>
13+
/// IP v4 and v6 range helper by jsakamoto
14+
/// Fork from https://github.com/jsakamoto/ipaddressrange
15+
/// </summary>
16+
/// <example>
17+
/// "192.168.0.0/24"
18+
/// "fe80::/10"
19+
/// "192.168.0.0/255.255.255.0"
20+
/// "192.168.0.0-192.168.0.255"
21+
/// </example>
22+
[Serializable]
23+
public class IPAddressRange : ISerializable
24+
{
25+
public IPAddress Begin { get; set; }
26+
27+
public IPAddress End { get; set; }
28+
29+
public IPAddressRange()
30+
{
31+
this.Begin = new IPAddress(0L);
32+
this.End = new IPAddress(0L);
33+
}
34+
35+
public IPAddressRange(string ipRangeString)
36+
{
37+
// remove all spaces.
38+
ipRangeString = ipRangeString.Replace(" ", "");
39+
40+
// Pattern 1. CIDR range: "192.168.0.0/24", "fe80::/10"
41+
var m1 = Regex.Match(ipRangeString, @"^(?<adr>[\da-f\.:]+)/(?<maskLen>\d+)$", RegexOptions.IgnoreCase);
42+
if (m1.Success)
43+
{
44+
var baseAdrBytes = IPAddress.Parse(m1.Groups["adr"].Value).GetAddressBytes();
45+
var maskBytes = Bits.GetBitMask(baseAdrBytes.Length, int.Parse(m1.Groups["maskLen"].Value));
46+
baseAdrBytes = Bits.And(baseAdrBytes, maskBytes);
47+
this.Begin = new IPAddress(baseAdrBytes);
48+
this.End = new IPAddress(Bits.Or(baseAdrBytes, Bits.Not(maskBytes)));
49+
return;
50+
}
51+
52+
// Pattern 2. Uni address: "127.0.0.1", ":;1"
53+
var m2 = Regex.Match(ipRangeString, @"^(?<adr>[\da-f\.:]+)$", RegexOptions.IgnoreCase);
54+
if (m2.Success)
55+
{
56+
this.Begin = this.End = IPAddress.Parse(ipRangeString);
57+
return;
58+
}
59+
60+
// Pattern 3. Begin end range: "169.258.0.0-169.258.0.255"
61+
var m3 = Regex.Match(ipRangeString, @"^(?<begin>[\da-f\.:]+)-(?<end>[\da-f\.:]+)$", RegexOptions.IgnoreCase);
62+
if (m3.Success)
63+
{
64+
this.Begin = IPAddress.Parse(m3.Groups["begin"].Value);
65+
this.End = IPAddress.Parse(m3.Groups["end"].Value);
66+
return;
67+
}
68+
69+
// Pattern 4. Bit mask range: "192.168.0.0/255.255.255.0"
70+
var m4 = Regex.Match(ipRangeString, @"^(?<adr>[\da-f\.:]+)/(?<bitmask>[\da-f\.:]+)$", RegexOptions.IgnoreCase);
71+
if (m4.Success)
72+
{
73+
var baseAdrBytes = IPAddress.Parse(m4.Groups["adr"].Value).GetAddressBytes();
74+
var maskBytes = IPAddress.Parse(m4.Groups["bitmask"].Value).GetAddressBytes();
75+
baseAdrBytes = Bits.And(baseAdrBytes, maskBytes);
76+
this.Begin = new IPAddress(baseAdrBytes);
77+
this.End = new IPAddress(Bits.Or(baseAdrBytes, Bits.Not(maskBytes)));
78+
return;
79+
}
80+
81+
throw new FormatException("Unknown IP range string.");
82+
}
83+
84+
protected IPAddressRange(SerializationInfo info, StreamingContext context)
85+
{
86+
var names = new List<string>();
87+
foreach (var item in info) names.Add(item.Name);
88+
89+
Func<string, IPAddress> deserialize = (name) => names.Contains(name) ?
90+
IPAddress.Parse(info.GetValue(name, typeof(object)).ToString()) :
91+
new IPAddress(0L);
92+
93+
this.Begin = deserialize("Begin");
94+
this.End = deserialize("End");
95+
}
96+
97+
public bool Contains(IPAddress ipaddress)
98+
{
99+
if (ipaddress.AddressFamily != this.Begin.AddressFamily) return false;
100+
var adrBytes = ipaddress.GetAddressBytes();
101+
return Bits.GE(this.Begin.GetAddressBytes(), adrBytes) && Bits.LE(this.End.GetAddressBytes(), adrBytes);
102+
}
103+
104+
public void GetObjectData(SerializationInfo info, StreamingContext context)
105+
{
106+
info.AddValue("Begin", this.Begin != null ? this.Begin.ToString() : "");
107+
info.AddValue("End", this.End != null ? this.End.ToString() : "");
108+
}
109+
}
110+
111+
internal static class Bits
112+
{
113+
internal static byte[] Not(byte[] bytes)
114+
{
115+
return bytes.Select(b => (byte)~b).ToArray();
116+
}
117+
118+
internal static byte[] And(byte[] A, byte[] B)
119+
{
120+
return A.Zip(B, (a, b) => (byte)(a & b)).ToArray();
121+
}
122+
123+
internal static byte[] Or(byte[] A, byte[] B)
124+
{
125+
return A.Zip(B, (a, b) => (byte)(a | b)).ToArray();
126+
}
127+
128+
internal static bool GE(byte[] A, byte[] B)
129+
{
130+
return A.Zip(B, (a, b) => a == b ? 0 : a < b ? 1 : -1)
131+
.SkipWhile(c => c == 0)
132+
.FirstOrDefault() >= 0;
133+
}
134+
135+
internal static bool LE(byte[] A, byte[] B)
136+
{
137+
return A.Zip(B, (a, b) => a == b ? 0 : a < b ? 1 : -1)
138+
.SkipWhile(c => c == 0)
139+
.FirstOrDefault() <= 0;
140+
}
141+
142+
internal static byte[] GetBitMask(int sizeOfBuff, int bitLen)
143+
{
144+
var maskBytes = new byte[sizeOfBuff];
145+
var bytesLen = bitLen / 8;
146+
var bitsLen = bitLen % 8;
147+
for (int i = 0; i < bytesLen; i++)
148+
{
149+
maskBytes[i] = 0xff;
150+
}
151+
if (bitsLen > 0) maskBytes[bytesLen] = (byte)~Enumerable.Range(1, 8 - bitsLen).Select(n => 1 << n - 1).Aggregate((a, b) => a | b);
152+
return maskBytes;
153+
}
154+
155+
}
156+
}

WebApiThrottle/RateLimit.cs

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
namespace WebApiThrottle
88
{
9+
[Serializable]
910
public class RateLimits
1011
{
1112
public long PerSecond { get; set; }

WebApiThrottle/RequestIndentity.cs

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace WebApiThrottle
99
/// <summary>
1010
/// Stores the client ip, key and endpoint
1111
/// </summary>
12+
[Serializable]
1213
public class RequestIndentity
1314
{
1415
public string ClientIp { get; set; }

WebApiThrottle/ThrottleCounter.cs

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace WebApiThrottle
99
/// <summary>
1010
/// Stores the initial access time and the numbers of calls made from that point
1111
/// </summary>
12+
[Serializable]
1213
public struct ThrottleCounter
1314
{
1415
public DateTime Timestamp { get; set; }

WebApiThrottle/ThrottleLogEntry.cs

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace WebApiThrottle
99
{
10+
[Serializable]
1011
public class ThrottleLogEntry
1112
{
1213
public string RequestId { get; set; }

WebApiThrottle/ThrottlePolicy.cs

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace WebApiThrottle
99
/// <summary>
1010
/// Rate limits policy
1111
/// </summary>
12+
[Serializable]
1213
public class ThrottlePolicy
1314
{
1415
/// <summary>

WebApiThrottle/ThrottlingHandler.cs

+41-3
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
102102
}
103103

104104
//enforce ip rate limit as is most specific
105-
if (Policy.IpRules != null && Policy.IpRules.Keys.Contains(identity.ClientIp))
105+
string ipRule = null;
106+
if (Policy.IpRules != null && ContainsIp(Policy.IpRules.Keys.ToList(), identity.ClientIp, out ipRule))
106107
{
107-
var limit = Policy.IpRules[identity.ClientIp].GetLimit(rateLimitPeriod);
108+
var limit = Policy.IpRules[ipRule].GetLimit(rateLimitPeriod);
108109
if (limit > 0) rateLimit = limit;
109110
}
110111

@@ -150,7 +151,7 @@ private ThrottleCounter ProcessRequest(ThrottlePolicy throttlePolicy, RequestInd
150151

151152
if (throttlePolicy.IpThrottling)
152153
{
153-
if (throttlePolicy.IpWhitelist != null && throttlePolicy.IpWhitelist.Contains(throttleEntry.ClientIp))
154+
if (throttlePolicy.IpWhitelist != null && ContainsIp(throttlePolicy.IpWhitelist, throttleEntry.ClientIp))
154155
{
155156
return throttleCounter;
156157
}
@@ -232,6 +233,43 @@ protected IPAddress GetClientIp(HttpRequestMessage request)
232233
return null;
233234
}
234235

236+
private bool ContainsIp(List<string> ipRules, string clientIp)
237+
{
238+
var ip = IPAddress.Parse(clientIp);
239+
if (ipRules != null && ipRules.Any())
240+
{
241+
foreach (var rule in ipRules)
242+
{
243+
var range = new IPAddressRange(rule);
244+
if (range.Contains(ip)) return true;
245+
}
246+
247+
}
248+
249+
return false;
250+
}
251+
252+
private bool ContainsIp(List<string> ipRules, string clientIp, out string rule)
253+
{
254+
rule = null;
255+
var ip = IPAddress.Parse(clientIp);
256+
if (ipRules != null && ipRules.Any())
257+
{
258+
foreach (var r in ipRules)
259+
{
260+
var range = new IPAddressRange(r);
261+
if (range.Contains(ip))
262+
{
263+
rule = r;
264+
return true;
265+
}
266+
}
267+
268+
}
269+
270+
return false;
271+
}
272+
235273
private Task<HttpResponseMessage> QuotaExceededResponse(HttpRequestMessage request, string message)
236274
{
237275
return Task.FromResult(request.CreateResponse(HttpStatusCode.Conflict, message));

WebApiThrottle/WebApiThrottle.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
<Reference Include="System.Xml" />
6262
</ItemGroup>
6363
<ItemGroup>
64+
<Compile Include="IPAddressRange.cs" />
6465
<Compile Include="IThrottleLogger.cs" />
6566
<Compile Include="IThrottleRepository.cs" />
6667
<Compile Include="Properties\AssemblyInfo.cs" />

0 commit comments

Comments
 (0)