using Swan.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace EmbedIO.Security
{
///
/// Represents a log message regex matching criterion for .
///
///
public class IPBanningRegexCriterion : IIPBanningCriterion
{
///
/// The default matching period.
///
public const int DefaultSecondsMatchingPeriod = 60;
///
/// The default maximum match count per period.
///
public const int DefaultMaxMatchCount = 10;
private readonly ConcurrentDictionary> _failRegexMatches = new ConcurrentDictionary>();
private readonly ConcurrentDictionary _failRegex = new ConcurrentDictionary();
private readonly IPBanningModule _parent;
private readonly int _secondsMatchingPeriod;
private readonly int _maxMatchCount;
private readonly ILogger? _innerLogger;
private bool _disposed;
///
/// Initializes a new instance of the class.
///
/// The parent.
/// The rules.
/// The maximum match count.
/// The seconds matching period.
public IPBanningRegexCriterion(IPBanningModule parent, IEnumerable rules, int maxMatchCount = DefaultMaxMatchCount, int secondsMatchingPeriod = DefaultSecondsMatchingPeriod)
{
_secondsMatchingPeriod = secondsMatchingPeriod;
_maxMatchCount = maxMatchCount;
_parent = parent;
AddRules(rules);
if (_failRegex.Any())
_innerLogger = new InnerRegexCriterionLogger(this);
}
///
/// Finalizes an instance of the class.
///
~IPBanningRegexCriterion()
{
Dispose(false);
}
///
public Task ValidateIPAddress(IPAddress address)
{
var minTime = DateTime.Now.AddSeconds(-1 * _secondsMatchingPeriod).Ticks;
var shouldBan = _failRegexMatches.TryGetValue(address, out var attempts) &&
attempts.Count(x => x >= minTime) >= _maxMatchCount;
return Task.FromResult(shouldBan);
}
///
public void ClearIPAddress(IPAddress address) =>
_failRegexMatches.TryRemove(address, out _);
///
public void PurgeData()
{
var minTime = DateTime.Now.AddSeconds(-1 * _secondsMatchingPeriod).Ticks;
foreach (var k in _failRegexMatches.Keys)
{
if (!_failRegexMatches.TryGetValue(k, out var failRegexMatches)) continue;
var recentMatches = new ConcurrentBag(failRegexMatches.Where(x => x >= minTime));
if (!recentMatches.Any())
_failRegexMatches.TryRemove(k, out _);
else
_failRegexMatches.AddOrUpdate(k, recentMatches, (x, y) => recentMatches);
}
}
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
_failRegexMatches.Clear();
_failRegex.Clear();
if (_innerLogger != null)
{
try
{
Logger.UnregisterLogger(_innerLogger);
}
catch
{
// ignore
}
_innerLogger.Dispose();
}
}
_disposed = true;
}
private void MatchIP(IPAddress address, string message)
{
if (!_parent.Configuration.ShouldContinue(address))
return;
foreach (var regex in _failRegex.Values)
{
try
{
if (!regex.IsMatch(message)) continue;
_failRegexMatches.GetOrAdd(address, new ConcurrentBag()).Add(DateTime.Now.Ticks);
break;
}
catch (RegexMatchTimeoutException ex)
{
$"Timeout trying to match '{ex.Input}' with pattern '{ex.Pattern}'.".Error(nameof(InnerRegexCriterionLogger));
}
}
}
private void AddRules(IEnumerable patterns)
{
foreach (var pattern in patterns)
AddRule(pattern);
}
private void AddRule(string pattern)
{
try
{
_failRegex.TryAdd(pattern, new Regex(pattern, RegexOptions.Compiled | RegexOptions.CultureInvariant, TimeSpan.FromMilliseconds(500)));
}
catch (Exception ex)
{
ex.Log(nameof(IPBanningModule), $"Invalid regex - '{pattern}'.");
}
}
private sealed class InnerRegexCriterionLogger : ILogger
{
private readonly IPBanningRegexCriterion _parent;
public InnerRegexCriterionLogger(IPBanningRegexCriterion parent)
{
_parent = parent;
Logger.RegisterLogger(this);
}
///
public LogLevel LogLevel => LogLevel.Trace;
public void Dispose()
{
// DO nothing
}
///
public void Log(LogMessageReceivedEventArgs logEvent)
{
var clientAddress = _parent._parent.ClientAddress;
if (clientAddress == null || string.IsNullOrWhiteSpace(logEvent.Message))
return;
_parent.MatchIP(clientAddress, logEvent.Message);
}
}
}
}