Skip to content

Commit 4d6ebd6

Browse files
committed
User name truncation
By default, all usernames will truncate at the '@' character. This can be configured differently if desired. I did some refactoring of regex parsing to move it out of the config class and into it's own class.
1 parent c7c3f73 commit 4d6ebd6

17 files changed

+481
-356
lines changed

IPBanCore/Core/IPBan/IPBanConfig.cs

+11-133
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2929
using System.Collections.Generic;
3030
using System.ComponentModel;
3131
using System.Diagnostics.CodeAnalysis;
32+
using System.Globalization;
3233
using System.IO;
3334
using System.Linq;
35+
using System.Net;
3436
using System.Text;
3537
using System.Text.RegularExpressions;
3638
using System.Xml;
@@ -43,7 +45,7 @@ namespace DigitalRuby.IPBanCore
4345
/// <summary>
4446
/// Configuration for ip ban app
4547
/// </summary>
46-
public class IPBanConfig : IIsWhitelisted
48+
public sealed class IPBanConfig : IIsWhitelisted
4749
{
4850
/// <summary>
4951
/// Allow temporary change of config
@@ -124,6 +126,7 @@ public void Dispose()
124126
private readonly bool clearBannedIPAddressesOnRestart;
125127
private readonly bool clearFailedLoginsOnSuccessfulLogin;
126128
private readonly bool processInternalIPAddresses;
129+
private readonly string truncateUserNameChars;
127130
private readonly HashSet<string> userNameWhitelist = new(StringComparer.Ordinal);
128131
private readonly int userNameWhitelistMaximumEditDistance = 2;
129132
private readonly Regex userNameWhitelistRegex;
@@ -190,6 +193,8 @@ private IPBanConfig(XmlDocument doc, IDnsLookup dns = null, IDnsServerList dnsLi
190193
TryGetConfig<bool>("ClearBannedIPAddressesOnRestart", ref clearBannedIPAddressesOnRestart);
191194
TryGetConfig<bool>("ClearFailedLoginsOnSuccessfulLogin", ref clearFailedLoginsOnSuccessfulLogin);
192195
TryGetConfig<bool>("ProcessInternalIPAddresses", ref processInternalIPAddresses);
196+
TryGetConfig<string>("TruncateUserNameChars", ref truncateUserNameChars);
197+
IPBanRegexParser.Instance.TruncateUserNameChars = truncateUserNameChars;
193198
GetConfig<TimeSpan>("ExpireTime", ref expireTime, TimeSpan.Zero, maxBanTimeSpan);
194199
if (expireTime.TotalMinutes < 1.0)
195200
{
@@ -403,138 +408,6 @@ private void ParseFirewallBlockRules()
403408
}
404409
}
405410

406-
/// <summary>
407-
/// Validate a regex - returns an error otherwise empty string if success
408-
/// </summary>
409-
/// <param name="regex">Regex to validate, can be null or empty</param>
410-
/// <param name="options">Regex options</param>
411-
/// <param name="throwException">True to throw the exception instead of returning the string, false otherwise</param>
412-
/// <returns>Null if success, otherwise an error string indicating the problem</returns>
413-
public static string ValidateRegex(string regex, RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, bool throwException = false)
414-
{
415-
try
416-
{
417-
if (regex != null)
418-
{
419-
_ = new Regex(regex, options);
420-
}
421-
return null;
422-
}
423-
catch (Exception ex)
424-
{
425-
if (throwException)
426-
{
427-
throw;
428-
}
429-
return ex.Message;
430-
}
431-
}
432-
433-
private static readonly Dictionary<string, Regex> regexCacheCompiled = new();
434-
private static readonly Dictionary<string, Regex> regexCacheNotCompiled = new();
435-
436-
/// <summary>
437-
/// Get a regex from text
438-
/// </summary>
439-
/// <param name="text">Text</param>
440-
/// <param name="multiline">Whether to use multi-line regex, default is false which is single line</param>
441-
/// <returns>Regex or null if text is null or whitespace</returns>
442-
public static Regex ParseRegex(string text, bool multiline = false)
443-
{
444-
const int maxCacheSize = 200;
445-
446-
text = (text ?? string.Empty).Trim();
447-
if (text.Length == 0)
448-
{
449-
return null;
450-
}
451-
452-
string[] lines = text.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
453-
StringBuilder sb = new();
454-
foreach (string line in lines)
455-
{
456-
sb.Append(line);
457-
}
458-
RegexOptions options = RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled;
459-
if (multiline)
460-
{
461-
options |= RegexOptions.Multiline;
462-
}
463-
string sbText = sb.ToString();
464-
string cacheKey = ((uint)options).ToString("X8") + ":" + sbText;
465-
466-
// allow up to maxCacheSize compiled dynamic regular expression, with minimal config changes/reload, this should last the lifetime of an app
467-
lock (regexCacheCompiled)
468-
{
469-
if (regexCacheCompiled.TryGetValue(cacheKey, out Regex value))
470-
{
471-
return value;
472-
}
473-
else if (regexCacheCompiled.Count < maxCacheSize)
474-
{
475-
value = new Regex(sbText, options);
476-
regexCacheCompiled.Add(cacheKey, value);
477-
return value;
478-
}
479-
}
480-
481-
// have to fall-back to non-compiled regex to avoid run-away memory usage
482-
try
483-
{
484-
lock (regexCacheNotCompiled)
485-
{
486-
if (regexCacheNotCompiled.TryGetValue(cacheKey, out Regex value))
487-
{
488-
return value;
489-
}
490-
491-
// strip compiled flag
492-
options &= (~RegexOptions.Compiled);
493-
value = new Regex(sbText, options);
494-
regexCacheNotCompiled.Add(cacheKey, value);
495-
return value;
496-
}
497-
}
498-
finally
499-
{
500-
// clear non-compield regex cache if it exceeds max size
501-
lock (regexCacheNotCompiled)
502-
{
503-
if (regexCacheNotCompiled.Count > maxCacheSize)
504-
{
505-
regexCacheNotCompiled.Clear();
506-
}
507-
}
508-
}
509-
}
510-
511-
/// <summary>
512-
/// Clean a multi-line string to make it more readable
513-
/// </summary>
514-
/// <param name="text">Multi-line string</param>
515-
/// <returns>Cleaned multi-line string</returns>
516-
public static string CleanMultilineString(string text)
517-
{
518-
text = (text ?? string.Empty).Trim();
519-
if (text.Length == 0)
520-
{
521-
return string.Empty;
522-
}
523-
524-
string[] lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries);
525-
StringBuilder sb = new();
526-
foreach (string line in lines)
527-
{
528-
string trimmedLine = line.Trim();
529-
if (trimmedLine.Length != 0)
530-
{
531-
sb.Append(trimmedLine);
532-
sb.Append('\n');
533-
}
534-
}
535-
return sb.ToString().Trim();
536-
}
537-
538411
/// <inheritdoc />
539412
public override string ToString()
540413
{
@@ -1005,6 +878,11 @@ appSettingsOverride is not null &&
1005878
/// </summary>
1006879
public IIPBanFilter BlacklistFilter => blacklistFilter;
1007880

881+
/// <summary>
882+
/// Characters to truncate user names at, empty for no truncation
883+
/// </summary>
884+
public string TruncateUserNameChars { get { return truncateUserNameChars; } }
885+
1008886
/// <summary>
1009887
/// White list user names. Any user name found not in the list is banned, unless the list is empty, in which case no checking is done.
1010888
/// If not empty, Any user name within 'UserNameWhitelistMinimumEditDistance' in the config is also not banned.

IPBanCore/Core/IPBan/IPBanConfigWindowsEventViewer.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public string XPath
7777
public XmlCData Regex
7878
{
7979
get => regex;
80-
set => RegexObject = IPBanConfig.ParseRegex(regex = value);
80+
set => RegexObject = IPBanRegexParser.ParseRegex(regex = value);
8181
}
8282

8383
/// <summary>
@@ -186,7 +186,7 @@ public void SetExpressionsFromExpressionsText()
186186
}
187187

188188
Expressions.Clear();
189-
string[] lines = IPBanConfig.CleanMultilineString(ExpressionsText).Split('\n');
189+
string[] lines = IPBanRegexParser.CleanMultilineString(ExpressionsText).Split('\n');
190190
string line;
191191
EventViewerExpression currentExpression = null;
192192
for (int i = 0; i < lines.Length; i++)

IPBanCore/Core/IPBan/IPBanFilter.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ await ExtensionMethods.RetryAsync(async () => addresses = await dns.GetHostAddre
247247

248248
if (!string.IsNullOrWhiteSpace(regexValue))
249249
{
250-
regex = IPBanConfig.ParseRegex(regexValue);
250+
regex = IPBanRegexParser.ParseRegex(regexValue);
251251
}
252252
}
253253

IPBanCore/Core/IPBan/IPBanLogFileManager.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,8 @@ private void UpdateLogFiles(IPBanConfig newConfig)
109109
MaxFileSizeBytes = newFile.MaxFileSize,
110110
PathAndMask = pathAndMask,
111111
PingIntervalMilliseconds = (service.ManualCycle ? 0 : newFile.PingInterval),
112-
RegexFailure = IPBanConfig.ParseRegex(newFile.FailedLoginRegex, true),
113-
RegexSuccess = IPBanConfig.ParseRegex(newFile.SuccessfulLoginRegex, true),
112+
RegexFailure = IPBanRegexParser.ParseRegex(newFile.FailedLoginRegex, true),
113+
RegexSuccess = IPBanRegexParser.ParseRegex(newFile.SuccessfulLoginRegex, true),
114114
RegexFailureTimestampFormat = newFile.FailedLoginRegexTimestampFormat,
115115
RegexSuccessTimestampFormat = newFile.SuccessfulLoginRegexTimestampFormat,
116116
Source = newFile.Source,

IPBanCore/Core/IPBan/IPBanLogFileScanner.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private void ParseRegex(Regex regex, string text, bool successful, string timest
119119
{
120120
List<IPAddressLogEvent> events = new();
121121
IPAddressEventType type = (successful ? IPAddressEventType.SuccessfulLogin : IPAddressEventType.FailedLogin);
122-
foreach (IPAddressLogEvent info in IPBanService.GetIPAddressEventsFromRegex(regex, text, timestampFormat, type, Source, dns))
122+
foreach (IPAddressLogEvent info in IPBanRegexParser.Instance.GetIPAddressEventsFromRegex(regex, text, timestampFormat, type, Source, dns))
123123
{
124124
info.Source ??= Source; // apply default source only if we don't already have a source
125125
if (info.FailedLoginThreshold <= 0)

IPBanCore/Core/IPBan/IPBanLogFileTester.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ public static void RunLogFileTest(string fileName,
6767
MaxFileSizeBytes = 0,
6868
PathAndMask = fileName.Trim(),
6969
PingIntervalMilliseconds = 0,
70-
RegexFailure = (File.Exists(regexFailureFile) && regexFailureFile.Length > 2 ? IPBanConfig.ParseRegex(File.ReadAllText(regexFailureFile)) : null),
70+
RegexFailure = (File.Exists(regexFailureFile) && regexFailureFile.Length > 2 ? IPBanRegexParser.ParseRegex(File.ReadAllText(regexFailureFile)) : null),
7171
RegexFailureTimestampFormat = regexFailureTimestampFormat.Trim('.'),
72-
RegexSuccess = (File.Exists(regexSuccessFile) && regexSuccessFile.Length > 2 ? IPBanConfig.ParseRegex(File.ReadAllText(regexSuccessFile)) : null),
72+
RegexSuccess = (File.Exists(regexSuccessFile) && regexSuccessFile.Length > 2 ? IPBanRegexParser.ParseRegex(File.ReadAllText(regexSuccessFile)) : null),
7373
RegexSuccessTimestampFormat = regexSuccessTimestampFormat.Trim('.'),
7474
Source = "test",
7575
SuccessfulLogLevel = LogLevel.Warning

0 commit comments

Comments
 (0)