Skip to content

Commit d368b1d

Browse files
k3z0geoperez
authored andcommitted
IPBanningModule (#420)
* IPBanningModule * Adding AccessAttempts * Extensions and constructors Basic test * IP Parsing * Min fix IP Parser * Performance improvements * FailRegex as static ConcurrentDictionary * Extension methods for init. Xmldoc. * Unit Tests * More unit test * Code Style * Fix extension methods * IPParserTest * Code Style
1 parent fc4eb94 commit d368b1d

File tree

10 files changed

+831
-3
lines changed

10 files changed

+831
-3
lines changed

EmbedIO.sln

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.26730.16
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.29609.76
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{97BC259A-4E78-4BA8-8F4D-2656BC78BB34}"
77
EndProject
88
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{73F25F81-0412-412E-89C9-BAD33E9BCCDE}"
99
ProjectSection(SolutionItems) = preProject
10-
.travis.yml = .travis.yml
1110
appveyor.yml = appveyor.yml
1211
LICENSE = LICENSE
1312
README.md = README.md

src/EmbedIO.Samples/Program.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
34
using System.IO;
45
using System.Threading;
56
using System.Threading.Tasks;
67
using EmbedIO.Actions;
78
using EmbedIO.Files;
9+
using EmbedIO.Security;
810
using EmbedIO.WebApi;
911
using Swan;
1012
using Swan.Logging;
@@ -60,6 +62,13 @@ private static WebServer CreateWebServer(string url)
6062
var server = new WebServer(o => o
6163
.WithUrlPrefix(url)
6264
.WithMode(HttpListenerMode.EmbedIO))
65+
.WithIPBanning(o => o
66+
.WithWhitelist(
67+
"",
68+
"172.16.16.124",
69+
"172.16.17.1/24",
70+
"192.168.1-2.2-5")
71+
.WithRules("(404 Not Found)+"), 5,5)
6372
.WithLocalSessionManager()
6473
.WithCors(
6574
// Origins, separated by comma without last slash

src/EmbedIO/Security/BannedInfo.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System.Net;
2+
3+
namespace EmbedIO.Security
4+
{
5+
/// <summary>
6+
/// Represents the info af a banned IP address.
7+
/// </summary>
8+
public class BannedInfo
9+
{
10+
/// <summary>
11+
/// Gets or sets the banned IP address.
12+
/// </summary>
13+
public IPAddress IPAddress { get; set; }
14+
15+
/// <summary>
16+
/// Gets or sets until when the IP will remain ban.
17+
/// </summary>
18+
public long BanUntil { get; set; }
19+
20+
/// <summary>
21+
/// Gets or sets a value indicating whether this instance was explicitly banned by user.
22+
/// </summary>
23+
public bool IsExplicit { get; set; }
24+
}
25+
}
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
using EmbedIO.Utilities;
2+
using Swan;
3+
using Swan.Logging;
4+
using Swan.Threading;
5+
using System;
6+
using System.Collections.Concurrent;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Net;
10+
using System.Text.RegularExpressions;
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
14+
namespace EmbedIO.Security
15+
{
16+
/// <summary>
17+
/// A module for ban IPs that show the malicious signs, based on scanning log messages.
18+
/// </summary>
19+
/// <seealso cref="WebModuleBase" />
20+
public class IPBanningModule : WebModuleBase, ILogger
21+
{
22+
/// <summary>
23+
/// The default ban time, in minutes.
24+
/// </summary>
25+
public const int DefaultBanTime = 30;
26+
27+
/// <summary>
28+
/// The default maximum retries per minute.
29+
/// </summary>
30+
public const int DefaultMaxRetry = 10;
31+
32+
private static readonly ConcurrentDictionary<IPAddress, ConcurrentBag<long>> AccessAttempts = new ConcurrentDictionary<IPAddress, ConcurrentBag<long>>();
33+
private static readonly ConcurrentDictionary<IPAddress, BannedInfo> Blacklist = new ConcurrentDictionary<IPAddress, BannedInfo>();
34+
private static readonly ConcurrentDictionary<string, Regex> FailRegex = new ConcurrentDictionary<string, Regex>();
35+
private static readonly PeriodicTask? Purger;
36+
37+
private readonly List<IPAddress> _whitelist = new List<IPAddress>();
38+
private readonly int _banTime;
39+
private readonly int _maxRetry;
40+
private bool _disposedValue;
41+
42+
static IPBanningModule()
43+
{
44+
Purger = new PeriodicTask(TimeSpan.FromMinutes(1), ct =>
45+
{
46+
PurgeBlackList();
47+
PurgeAccessAttempts();
48+
49+
return Task.CompletedTask;
50+
});
51+
}
52+
53+
/// <summary>
54+
/// Initializes a new instance of the <see cref="IPBanningModule"/> class.
55+
/// </summary>
56+
/// <param name="baseRoute">The base route.</param>
57+
/// <param name="failRegex">A collection of regex to match the log messages against.</param>
58+
/// <param name="banTime">The time that an IP will remain ban, in minutes.</param>
59+
/// <param name="maxRetry">The maximum number of failed attempts before banning an IP.</param>
60+
public IPBanningModule(string baseRoute,
61+
IEnumerable<string> failRegex,
62+
int banTime = DefaultBanTime,
63+
int maxRetry = DefaultMaxRetry)
64+
: this(baseRoute, failRegex, null, banTime, maxRetry)
65+
{
66+
}
67+
68+
/// <summary>
69+
/// Initializes a new instance of the <see cref="IPBanningModule"/> class.
70+
/// </summary>
71+
/// <param name="baseRoute">The base route.</param>
72+
/// <param name="failRegex">A collection of regex to match the log messages against.</param>
73+
/// <param name="whitelist">A collection of valid IPs that never will be banned.</param>
74+
/// <param name="banTime">The time that an IP will remain ban, in minutes.</param>
75+
/// <param name="maxRetry">The maximum number of failed attempts before banning an IP.</param>
76+
public IPBanningModule(string baseRoute,
77+
IEnumerable<string>? failRegex = null,
78+
IEnumerable<string>? whitelist = null,
79+
int banTime = DefaultBanTime,
80+
int maxRetry = DefaultMaxRetry)
81+
: base(baseRoute)
82+
{
83+
if (failRegex != null)
84+
AddRules(failRegex);
85+
86+
_banTime = banTime;
87+
_maxRetry = maxRetry;
88+
AddToWhitelist(whitelist);
89+
Logger.RegisterLogger(this);
90+
}
91+
92+
/// <inheritdoc />
93+
public override bool IsFinalHandler => false;
94+
95+
/// <inheritdoc />
96+
public LogLevel LogLevel => LogLevel.Trace;
97+
98+
private IPAddress? ClientAddress { get; set; }
99+
100+
/// <summary>
101+
/// Gets the list of current banned IPs.
102+
/// </summary>
103+
/// <returns>A collection of <see cref="BannedInfo"/> in the blacklist.</returns>
104+
public static IEnumerable<BannedInfo> GetBannedIPs() =>
105+
Blacklist.Values.ToList();
106+
107+
/// <summary>
108+
/// Tries to ban an IP explicitly.
109+
/// </summary>
110+
/// <param name="address">The IP address to ban.</param>
111+
/// <param name="minutes">The time in minutes that the IP will remain ban.</param>
112+
/// <param name="isExplicit">if set to <c>true</c> [is explicit].</param>
113+
/// <returns>
114+
/// <c>true</c> if the IP was added to the blacklist; otherwise, <c>false</c>.
115+
/// </returns>
116+
public static bool TryBanIP(IPAddress address, int minutes, bool isExplicit = true) =>
117+
TryBanIP(address, DateTime.Now.AddMinutes(minutes), isExplicit);
118+
119+
/// <summary>
120+
/// Tries to ban an IP explicitly.
121+
/// </summary>
122+
/// <param name="address">The IP address to ban.</param>
123+
/// <param name="banTime">An <see cref="TimeSpan"/> that sets the time the IP will remain ban.</param>
124+
/// <param name="isExplicit">if set to <c>true</c> [is explicit].</param>
125+
/// <returns>
126+
/// <c>true</c> if the IP was added to the blacklist; otherwise, <c>false</c>.
127+
/// </returns>
128+
public static bool TryBanIP(IPAddress address, TimeSpan banTime, bool isExplicit = true) =>
129+
TryBanIP(address, DateTime.Now.Add(banTime), isExplicit);
130+
131+
/// <summary>
132+
/// Tries to ban an IP explicitly.
133+
/// </summary>
134+
/// <param name="address">The IP address to ban.</param>
135+
/// <param name="banUntil">A <see cref="DateTime"/> that sets until when the IP will remain ban.</param>
136+
/// <param name="isExplicit">if set to <c>true</c> [is explicit].</param>
137+
/// <returns>
138+
/// <c>true</c> if the IP was added to the blacklist; otherwise, <c>false</c>.
139+
/// </returns>
140+
public static bool TryBanIP(IPAddress address, DateTime banUntil, bool isExplicit = true)
141+
{
142+
if (Blacklist.ContainsKey(address))
143+
{
144+
var bannedInfo = Blacklist[address];
145+
bannedInfo.BanUntil = banUntil.Ticks;
146+
bannedInfo.IsExplicit = isExplicit;
147+
148+
return true;
149+
}
150+
151+
return Blacklist.TryAdd(address, new BannedInfo()
152+
{
153+
IPAddress = address,
154+
BanUntil = banUntil.Ticks,
155+
IsExplicit = isExplicit,
156+
});
157+
}
158+
159+
/// <summary>
160+
/// Tries to unban an IP explicitly.
161+
/// </summary>
162+
/// <param name="address">The IP address.</param>
163+
/// <returns>
164+
/// <c>true</c> if the IP was removed from the blacklist; otherwise, <c>false</c>.
165+
/// </returns>
166+
public static bool TryUnbanIP(IPAddress address) =>
167+
Blacklist.TryRemove(address, out _);
168+
169+
/// <inheritdoc />
170+
public void Log(LogMessageReceivedEventArgs logEvent)
171+
{
172+
// Process Log
173+
if (string.IsNullOrWhiteSpace(logEvent.Message) ||
174+
ClientAddress == null ||
175+
!FailRegex.Any() ||
176+
_whitelist.Contains(ClientAddress) ||
177+
Blacklist.ContainsKey(ClientAddress))
178+
return;
179+
180+
foreach (var regex in FailRegex.Values)
181+
{
182+
try
183+
{
184+
if (!regex.IsMatch(logEvent.Message)) continue;
185+
186+
// Add to list
187+
AddAccessAttempt(ClientAddress);
188+
UpdateBlackList();
189+
break;
190+
}
191+
catch (RegexMatchTimeoutException ex)
192+
{
193+
$"Timeout trying to match '{ex.Input}' with pattern '{ex.Pattern}'.".Error(nameof(IPBanningModule));
194+
}
195+
}
196+
}
197+
198+
/// <inheritdoc />
199+
public void Dispose() =>
200+
Dispose(true);
201+
202+
internal void AddRules(IEnumerable<string> patterns)
203+
{
204+
foreach (var pattern in patterns)
205+
AddRule(pattern);
206+
}
207+
208+
internal void AddRule(string pattern)
209+
{
210+
try
211+
{
212+
FailRegex.TryAdd(pattern, new Regex(pattern, RegexOptions.Compiled | RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(500)));
213+
}
214+
catch (Exception ex)
215+
{
216+
ex.Log(nameof(IPBanningModule), $"Invalid regex - '{pattern}'.");
217+
}
218+
}
219+
220+
internal void AddToWhitelist(IEnumerable<string> whitelist) =>
221+
AddToWhitelistAsync(whitelist).GetAwaiter().GetResult();
222+
223+
internal async Task AddToWhitelistAsync(IEnumerable<string> whitelist)
224+
{
225+
if (whitelist?.Any() != true)
226+
return;
227+
228+
foreach (var address in whitelist)
229+
{
230+
var addressees = await IPParser.Parse(address).ConfigureAwait(false);
231+
_whitelist.AddRange(addressees.Where(x => !_whitelist.Contains(x)));
232+
}
233+
}
234+
235+
/// <inheritdoc />
236+
protected override Task OnRequestAsync(IHttpContext context)
237+
{
238+
ClientAddress = context.Request.RemoteEndPoint.Address;
239+
if (!Blacklist.ContainsKey(ClientAddress))
240+
return Task.CompletedTask;
241+
242+
context.SetHandled();
243+
throw HttpException.Forbidden();
244+
}
245+
246+
/// <summary>
247+
/// Releases unmanaged and - optionally - managed resources.
248+
/// </summary>
249+
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
250+
protected virtual void Dispose(bool disposing)
251+
{
252+
if (_disposedValue) return;
253+
if (disposing)
254+
{
255+
_whitelist.Clear();
256+
}
257+
258+
_disposedValue = true;
259+
}
260+
261+
private static void AddAccessAttempt(IPAddress address)
262+
{
263+
if (AccessAttempts.ContainsKey(address))
264+
AccessAttempts[address].Add(DateTime.Now.Ticks);
265+
else
266+
AccessAttempts.TryAdd(address, new ConcurrentBag<long>() { DateTime.Now.Ticks });
267+
}
268+
269+
private static void PurgeBlackList()
270+
{
271+
foreach (var k in Blacklist.Keys)
272+
{
273+
if (DateTime.Now.Ticks > Blacklist[k].BanUntil)
274+
Blacklist.TryRemove(k, out _);
275+
}
276+
}
277+
278+
private static void PurgeAccessAttempts()
279+
{
280+
var banDate = DateTime.Now.AddMinutes(-1).Ticks;
281+
282+
foreach (var k in AccessAttempts.Keys)
283+
{
284+
var recentAttempts = new ConcurrentBag<long>(AccessAttempts[k].Where(x => x >= banDate));
285+
if (!recentAttempts.Any())
286+
AccessAttempts.TryRemove(k, out _);
287+
else
288+
Interlocked.Exchange(ref recentAttempts, AccessAttempts[k]);
289+
}
290+
}
291+
292+
private void UpdateBlackList()
293+
{
294+
var time = DateTime.Now.AddMinutes(-1).Ticks;
295+
if ((AccessAttempts[ClientAddress]?.Where(x => x >= time).Count() >= _maxRetry))
296+
{
297+
TryBanIP(ClientAddress, _banTime, false);
298+
}
299+
}
300+
}
301+
}

0 commit comments

Comments
 (0)