Adds assemblies config, fixes crash bugs. (#134)

This commit is contained in:
Kamron Batman 2020-05-09 12:55:56 -07:00 • committed by GitHub
parent 0140eb73b4
commit 8ec166bcd0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3223 changed files with 191 additions and 191 deletions

View file

@ -0,0 +1,46 @@
using System;
using System.IO;
using System.Net;
using Server.Misc;
namespace Server
{
public class AccessRestrictions
{
public static void Initialize()
{
EventSink.SocketConnect += EventSink_SocketConnect;
}
private static void EventSink_SocketConnect(SocketConnectEventArgs e)
{
try
{
IPAddress ip = ((IPEndPoint)e.Context.RemoteEndPoint).Address;
if (Firewall.IsBlocked(ip))
{
Console.WriteLine("Client: {0}: Firewall blocked connection attempt.", ip);
e.AllowConnection = false;
return;
}
if (IPLimiter.SocketBlock && !IPLimiter.Verify(ip))
{
Console.WriteLine("Client: {0}: Past IP limit threshold", ip);
using (StreamWriter op = new StreamWriter("ipLimits.log", true))
{
op.WriteLine("{0}\tPast IP limit threshold\t{1}", ip, DateTime.UtcNow);
}
e.AllowConnection = false;
}
}
catch
{
e.AllowConnection = false;
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using Server.Network;
namespace Server.Accounting
{
public class AccountAttackLimiter
{
public static bool Enabled;
public static void Configure()
{
Enabled = ServerConfiguration.GetOrUpdateSetting("accountAttackLimiter.enable", true);
}
private static readonly List<InvalidAccountAccessLog> m_List = new List<InvalidAccountAccessLog>();
public static void Initialize()
{
if (!Enabled)
return;
PacketHandlers.RegisterThrottler(0x80, Throttle_Callback);
PacketHandlers.RegisterThrottler(0x91, Throttle_Callback);
PacketHandlers.RegisterThrottler(0xCF, Throttle_Callback);
}
public static TimeSpan Throttle_Callback(NetState ns)
{
InvalidAccountAccessLog accessLog = FindAccessLog(ns);
if (accessLog == null)
return TimeSpan.Zero;
DateTime date = DateTime.UtcNow;
DateTime access = accessLog.LastAccessTime + ComputeThrottle(accessLog.Counts);
return date >= access ? TimeSpan.Zero : date - access;
}
public static InvalidAccountAccessLog FindAccessLog(NetState ns)
{
if (ns == null)
return null;
IPAddress ipAddress = ns.Address;
for (int i = 0; i < m_List.Count; ++i)
{
InvalidAccountAccessLog accessLog = m_List[i];
if (accessLog.HasExpired)
m_List.RemoveAt(i--);
else if (accessLog.Address.Equals(ipAddress))
return accessLog;
}
return null;
}
public static void RegisterInvalidAccess(NetState ns)
{
if (ns == null || !Enabled)
return;
InvalidAccountAccessLog accessLog = FindAccessLog(ns);
if (accessLog == null)
m_List.Add(accessLog = new InvalidAccountAccessLog(ns.Address));
accessLog.Counts += 1;
accessLog.RefreshAccessTime();
if (accessLog.Counts >= 3)
try
{
using StreamWriter op = new StreamWriter("throttle.log", true);
op.WriteLine(
"{0}\t{1}\t{2}",
DateTime.UtcNow,
ns,
accessLog.Counts);
}
catch
{
// ignored
}
}
public static TimeSpan ComputeThrottle(int counts)
{
if (counts >= 15)
return TimeSpan.FromMinutes(5.0);
if (counts >= 10)
return TimeSpan.FromMinutes(1.0);
if (counts >= 5)
return TimeSpan.FromSeconds(20.0);
if (counts >= 3)
return TimeSpan.FromSeconds(10.0);
if (counts >= 1)
return TimeSpan.FromSeconds(2.0);
return TimeSpan.Zero;
}
}
public class InvalidAccountAccessLog
{
public InvalidAccountAccessLog(IPAddress address)
{
Address = address;
RefreshAccessTime();
}
public IPAddress Address { get; set; }
public DateTime LastAccessTime { get; set; }
public bool HasExpired => DateTime.UtcNow >= LastAccessTime + TimeSpan.FromHours(1.0);
public int Counts { get; set; }
public void RefreshAccessTime()
{
LastAccessTime = DateTime.UtcNow;
}
}
}

View file

@ -0,0 +1,73 @@
using System;
using System.Xml;
namespace Server.Accounting
{
public class AccountComment
{
private string m_Content;
/// <summary>
/// Constructs a new AccountComment instance.
/// </summary>
/// <param name="addedBy">Initial AddedBy value.</param>
/// <param name="content">Initial Content value.</param>
public AccountComment(string addedBy, string content)
{
AddedBy = addedBy;
m_Content = content;
LastModified = DateTime.UtcNow;
}
/// <summary>
/// Deserializes an AccountComment instance from an xml element.
/// </summary>
/// <param name="node">The XmlElement instance from which to deserialize.</param>
public AccountComment(XmlElement node)
{
AddedBy = Utility.GetAttribute(node, "addedBy", "empty");
LastModified = Utility.GetXMLDateTime(Utility.GetAttribute(node, "lastModified"), DateTime.UtcNow);
m_Content = Utility.GetText(node, "");
}
/// <summary>
/// A string representing who added this comment.
/// </summary>
public string AddedBy { get; }
/// <summary>
/// Gets or sets the body of this comment. Setting this value will reset LastModified.
/// </summary>
public string Content
{
get => m_Content;
set
{
m_Content = value;
LastModified = DateTime.UtcNow;
}
}
/// <summary>
/// The date and time when this account was last modified -or- the comment creation time, if never modified.
/// </summary>
public DateTime LastModified { get; private set; }
/// <summary>
/// Serializes this AccountComment instance to an XmlTextWriter.
/// </summary>
/// <param name="xml">The XmlTextWriter instance from which to serialize.</param>
public void Save(XmlTextWriter xml)
{
xml.WriteStartElement("comment");
xml.WriteAttributeString("addedBy", AddedBy);
xml.WriteAttributeString("lastModified", XmlConvert.ToString(LastModified, XmlDateTimeSerializationMode.Utc));
xml.WriteString(m_Content);
xml.WriteEndElement();
}
}
}

View file

@ -0,0 +1,415 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using Server.Accounting;
using Server.Engines.Help;
using Server.Network;
using Server.Regions;
namespace Server.Misc
{
public class AccountHandler
{
private static int MaxAccountsPerIP;
private static bool AutoAccountCreation;
private static bool RestrictDeletion = !TestCenter.Enabled;
private static TimeSpan DeleteDelay = TimeSpan.FromDays(7.0);
private static bool PasswordCommandEnabled;
public static void Configure()
{
MaxAccountsPerIP = ServerConfiguration.GetOrUpdateSetting("accountHandler.maxAccountsPerIP", 1);
AutoAccountCreation = ServerConfiguration.GetOrUpdateSetting("accountHandler.enableAutoAccountCreation", true);
PasswordCommandEnabled = ServerConfiguration.GetOrUpdateSetting("accountHandler.enablePlayerPasswordCommand", false);
}
private static readonly CityInfo[] StartingCities =
{
new CityInfo("New Haven", "New Haven Bank", 1150168, 3667, 2625, 0),
new CityInfo("Yew", "The Empath Abbey", 1075072, 633, 858, 0),
new CityInfo("Minoc", "The Barnacle", 1075073, 2476, 413, 15),
new CityInfo("Britain", "The Wayfarer's Inn", 1075074, 1602, 1591, 20),
new CityInfo("Moonglow", "The Scholars Inn", 1075075, 4408, 1168, 0),
new CityInfo("Trinsic", "The Traveler's Inn", 1075076, 1845, 2745, 0),
new CityInfo("Jhelom", "The Mercenary Inn", 1075078, 1374, 3826, 0),
new CityInfo("Skara Brae", "The Falconer's Inn", 1075079, 618, 2234, 0),
new CityInfo("Vesper", "The Ironwood Inn", 1075080, 2771, 976, 0)
};
/* Old Haven/Magincia Locations
new CityInfo( "Britain", "Sweet Dreams Inn", 1496, 1628, 10 );
// ..
// Trinsic
new CityInfo( "Magincia", "The Great Horns Tavern", 3734, 2222, 20 ),
// Jhelom
// ..
new CityInfo( "Haven", "Buckler's Hideaway", 3667, 2625, 0 )
if (Core.AOS)
{
//CityInfo haven = new CityInfo( "Haven", "Uzeraan's Mansion", 3618, 2591, 0 );
CityInfo haven = new CityInfo( "Haven", "Uzeraan's Mansion", 3503, 2574, 14 );
StartingCities[StartingCities.Length - 1] = haven;
}
*/
private static Dictionary<IPAddress, int> m_IPTable;
private static readonly char[] m_ForbiddenChars =
{
'<', '>', ':', '"', '/', '\\', '|', '?', '*'
};
public static AccessLevel LockdownLevel { get; set; }
public static Dictionary<IPAddress, int> IPTable
{
get
{
if (m_IPTable == null)
{
m_IPTable = new Dictionary<IPAddress, int>();
foreach (Account a in Accounts.GetAccounts())
if (a.LoginIPs.Length > 0)
{
IPAddress ip = a.LoginIPs[0];
m_IPTable[ip] = (m_IPTable.TryGetValue(ip, out int value) ? value : 0) + 1;
}
}
return m_IPTable;
}
}
public static void Initialize()
{
EventSink.DeleteRequest += EventSink_DeleteRequest;
EventSink.AccountLogin += EventSink_AccountLogin;
EventSink.GameLogin += EventSink_GameLogin;
if (PasswordCommandEnabled)
CommandSystem.Register("Password", AccessLevel.Player, Password_OnCommand);
}
[Usage("Password <newPassword> <repeatPassword>")]
[Description(
"Changes the password of the commanding players account. Requires the same C-class IP address as the account's creator.")]
public static void Password_OnCommand(CommandEventArgs e)
{
Mobile from = e.Mobile;
if (!(from.Account is Account acct))
return;
IPAddress[] accessList = acct.LoginIPs;
if (accessList.Length == 0)
return;
NetState ns = from.NetState;
if (ns == null)
return;
if (e.Length == 0)
{
from.SendMessage("You must specify the new password.");
return;
}
if (e.Length == 1)
{
from.SendMessage("To prevent potential typing mistakes, you must type the password twice. Use the format:");
from.SendMessage("Password \"(newPassword)\" \"(repeated)\"");
return;
}
string pass = e.GetString(0);
string pass2 = e.GetString(1);
if (pass != pass2)
{
from.SendMessage("The passwords do not match.");
return;
}
bool isSafe = true;
for (int i = 0; isSafe && i < pass.Length; ++i)
isSafe = pass[i] >= 0x20 && pass[i] < 0x7F;
if (!isSafe)
{
from.SendMessage("That is not a valid password.");
return;
}
try
{
IPAddress ipAddress = ns.Address;
if (Utility.IPMatchClassC(accessList[0], ipAddress))
{
acct.SetPassword(pass);
from.SendMessage("The password to your account has changed.");
}
else
{
PageEntry entry = PageQueue.GetEntry(from);
if (entry != null)
{
if (entry.Message.StartsWith("[Automated: Change Password]"))
from.SendMessage("You already have a password change request in the help system queue.");
else
from.SendMessage("Your IP address does not match that which created this account.");
}
else if (PageQueue.CheckAllowedToPage(from))
{
from.SendMessage(
"Your IP address does not match that which created this account. A page has been entered into the help system on your behalf.");
/* The next available Counselor/Game Master will respond as soon as possible.
* Please check your Journal for messages every few minutes.
*/
from.SendLocalizedMessage(501234, "", 0x35);
PageQueue.Enqueue(new PageEntry(from,
$"[Automated: Change Password]<br>Desired password: {pass}<br>Current IP address: {ipAddress}<br>Account IP address: {accessList[0]}",
PageType.Account));
}
}
}
catch
{
// ignored
}
}
private static void EventSink_DeleteRequest(NetState state, int index)
{
if (!(state.Account is Account acct))
{
state.Dispose();
}
else if (index < 0 || index >= acct.Length)
{
state.Send(new DeleteResult(DeleteResultType.BadRequest));
state.Send(new CharacterListUpdate(acct));
}
else
{
Mobile m = acct[index];
if (m == null)
{
state.Send(new DeleteResult(DeleteResultType.CharNotExist));
state.Send(new CharacterListUpdate(acct));
}
else if (m.NetState != null)
{
state.Send(new DeleteResult(DeleteResultType.CharBeingPlayed));
state.Send(new CharacterListUpdate(acct));
}
else if (RestrictDeletion && DateTime.UtcNow < m.CreationTime + DeleteDelay)
{
state.Send(new DeleteResult(DeleteResultType.CharTooYoung));
state.Send(new CharacterListUpdate(acct));
}
else if (m.AccessLevel == AccessLevel.Player &&
Region.Find(m.LogoutLocation, m.LogoutMap).IsPartOf<Jail>()) // Don't need to check current location, if netstate is null, they're logged out
{
state.Send(new DeleteResult(DeleteResultType.BadRequest));
state.Send(new CharacterListUpdate(acct));
}
else
{
Console.WriteLine("Client: {0}: Deleting character {1} (0x{2:X})", state, index, m.Serial.Value);
acct.Comments.Add(new AccountComment("System", $"Character #{index + 1} {m} deleted by {state}"));
m.Delete();
state.Send(new CharacterListUpdate(acct));
}
}
}
public static bool CanCreate(IPAddress ip)
{
if (!IPTable.ContainsKey(ip))
return true;
return IPTable[ip] < MaxAccountsPerIP;
}
private static bool IsForbiddenChar(char c)
{
for (int i = 0; i < m_ForbiddenChars.Length; ++i)
if (c == m_ForbiddenChars[i])
return true;
return false;
}
private static Account CreateAccount(NetState state, string un, string pw)
{
if (un.Length == 0 || pw.Length == 0)
return null;
bool isSafe = !(un.StartsWith(" ") || un.EndsWith(" ") || un.EndsWith("."));
for (int i = 0; isSafe && i < un.Length; ++i)
isSafe = un[i] >= 0x20 && un[i] < 0x7F && !IsForbiddenChar(un[i]);
for (int i = 0; isSafe && i < pw.Length; ++i)
isSafe = pw[i] >= 0x20 && pw[i] < 0x7F;
if (!isSafe)
return null;
if (!CanCreate(state.Address))
{
Console.WriteLine("Login: {0}: Account '{1}' not created, ip already has {2} account{3}.", state, un,
MaxAccountsPerIP, MaxAccountsPerIP == 1 ? "" : "s");
return null;
}
Console.WriteLine("Login: {0}: Creating new account '{1}'", state, un);
Account a = new Account(un, pw);
return a;
}
public static void EventSink_AccountLogin(AccountLoginEventArgs e)
{
if (!IPLimiter.SocketBlock && !IPLimiter.Verify(e.State.Address))
{
e.Accepted = false;
e.RejectReason = ALRReason.InUse;
Console.WriteLine("Login: {0}: Past IP limit threshold", e.State);
using (StreamWriter op = new StreamWriter("ipLimits.log", true))
{
op.WriteLine("{0}\tPast IP limit threshold\t{1}", e.State, DateTime.UtcNow);
}
return;
}
string un = e.Username;
string pw = e.Password;
e.Accepted = false;
if (!(Accounts.GetAccount(un) is Account acct))
{
// To prevent someone from making an account of just '' or a bunch of meaningless spaces
if (AutoAccountCreation && un.Trim().Length > 0)
{
e.State.Account = acct = CreateAccount(e.State, un, pw);
e.Accepted = acct?.CheckAccess(e.State) ?? false;
if (!e.Accepted)
e.RejectReason = ALRReason.BadComm;
}
else
{
Console.WriteLine("Login: {0}: Invalid username '{1}'", e.State, un);
e.RejectReason = ALRReason.Invalid;
}
}
else if (!acct.HasAccess(e.State))
{
Console.WriteLine("Login: {0}: Access denied for '{1}'", e.State, un);
e.RejectReason = LockdownLevel > AccessLevel.Player ? ALRReason.BadComm : ALRReason.BadPass;
}
else if (!acct.CheckPassword(pw))
{
Console.WriteLine("Login: {0}: Invalid password for '{1}'", e.State, un);
e.RejectReason = ALRReason.BadPass;
}
else if (acct.Banned)
{
Console.WriteLine("Login: {0}: Banned account '{1}'", e.State, un);
e.RejectReason = ALRReason.Blocked;
}
else
{
Console.WriteLine("Login: {0}: Valid credentials for '{1}'", e.State, un);
e.State.Account = acct;
e.Accepted = true;
acct.LogAccess(e.State);
}
if (!e.Accepted)
AccountAttackLimiter.RegisterInvalidAccess(e.State);
}
public static void EventSink_GameLogin(GameLoginEventArgs e)
{
if (!IPLimiter.SocketBlock && !IPLimiter.Verify(e.State.Address))
{
e.Accepted = false;
Console.WriteLine("Login: {0}: Past IP limit threshold", e.State);
using (StreamWriter op = new StreamWriter("ipLimits.log", true))
{
op.WriteLine("{0}\tPast IP limit threshold\t{1}", e.State, DateTime.UtcNow);
}
return;
}
string un = e.Username;
string pw = e.Password;
if (!(Accounts.GetAccount(un) is Account acct))
{
e.Accepted = false;
}
else if (!acct.HasAccess(e.State))
{
Console.WriteLine("Login: {0}: Access denied for '{1}'", e.State, un);
e.Accepted = false;
}
else if (!acct.CheckPassword(pw))
{
Console.WriteLine("Login: {0}: Invalid password for '{1}'", e.State, un);
e.Accepted = false;
}
else if (acct.Banned)
{
Console.WriteLine("Login: {0}: Banned account '{1}'", e.State, un);
e.Accepted = false;
}
else
{
acct.LogAccess(e.State);
Console.WriteLine("Login: {0}: Account '{1}' at character list", e.State, un);
e.State.Account = acct;
e.Accepted = true;
e.CityInfo = StartingCities;
}
if (!e.Accepted)
AccountAttackLimiter.RegisterInvalidAccess(e.State);
}
public static bool CheckAccount(Mobile mobCheck, Mobile accCheck)
{
if (accCheck?.Account is Account a)
for (int i = 0; i < a.Length; ++i)
if (a[i] == mobCheck)
return true;
return false;
}
}
}

View file

@ -0,0 +1,50 @@
using System.Xml;
namespace Server.Accounting
{
public class AccountTag
{
/// <summary>
/// Constructs a new AccountTag instance with a specific name and value.
/// </summary>
/// <param name="name">Initial name.</param>
/// <param name="value">Initial value.</param>
public AccountTag(string name, string value)
{
Name = name;
Value = value;
}
/// <summary>
/// Deserializes an AccountTag instance from an xml element.
/// </summary>
/// <param name="node">The XmlElement instance from which to deserialize.</param>
public AccountTag(XmlElement node)
{
Name = Utility.GetAttribute(node, "name", "empty");
Value = Utility.GetText(node, "");
}
/// <summary>
/// Gets or sets the name of this tag.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the value of this tag.
/// </summary>
public string Value { get; set; }
/// <summary>
/// Serializes this AccountTag instance to an XmlTextWriter.
/// </summary>
/// <param name="xml">The XmlTextWriter instance from which to serialize.</param>
public void Save(XmlTextWriter xml)
{
xml.WriteStartElement("tag");
xml.WriteAttributeString("name", Name);
xml.WriteString(Value);
xml.WriteEndElement();
}
}
}

View file

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
namespace Server.Accounting
{
public class Accounts
{
private static Dictionary<string, IAccount> m_Accounts = new Dictionary<string, IAccount>();
static Accounts()
{
}
public static int Count => m_Accounts.Count;
public static void Configure()
{
EventSink.WorldLoad += Load;
EventSink.WorldSave += Save;
}
public static IEnumerable<IAccount> GetAccounts() => m_Accounts.Values;
public static IAccount GetAccount(string username)
{
m_Accounts.TryGetValue(username, out IAccount a);
return a;
}
public static void Add(IAccount a)
{
m_Accounts[a.Username] = a;
}
public static void Remove(string username)
{
m_Accounts.Remove(username);
}
public static void Load()
{
m_Accounts = new Dictionary<string, IAccount>(32, StringComparer.OrdinalIgnoreCase);
string filePath = Path.Combine("Saves/Accounts", "accounts.xml");
if (!File.Exists(filePath))
return;
XmlDocument doc = new XmlDocument();
doc.Load(filePath);
XmlElement root = doc["accounts"];
foreach (XmlElement account in root.GetElementsByTagName("account"))
try
{
new Account(account);
}
catch
{
Console.WriteLine("Warning: Account instance load failed");
}
}
public static void Save(bool message)
{
if (!Directory.Exists("Saves/Accounts"))
Directory.CreateDirectory("Saves/Accounts");
string filePath = Path.Combine("Saves/Accounts", "accounts.xml");
using StreamWriter op = new StreamWriter(filePath);
XmlTextWriter xml = new XmlTextWriter(op) { Formatting = Formatting.Indented, IndentChar = '\t', Indentation = 1 };
xml.WriteStartDocument(true);
xml.WriteStartElement("accounts");
xml.WriteAttributeString("count", m_Accounts.Count.ToString());
foreach (Account a in GetAccounts())
a.Save(xml);
xml.WriteEndElement();
xml.Close();
}
}
}

View file

@ -0,0 +1,270 @@
using System.Collections.Generic;
using System.IO;
using System.Net;
namespace Server
{
public class Firewall
{
static Firewall()
{
List = new List<IFirewallEntry>();
string path = "firewall.cfg";
if (File.Exists(path))
{
using StreamReader ip = new StreamReader(path);
string line;
while ((line = ip.ReadLine()) != null)
{
line = line.Trim();
if (line.Length == 0)
continue;
List.Add(ToFirewallEntry(line));
/*
object toAdd;
IPAddress addr;
if (IPAddress.TryParse( line, out addr ))
toAdd = addr;
else
toAdd = line;
m_Blocked.Add( toAdd.ToString() );
* */
}
}
}
public static List<IFirewallEntry> List { get; }
public static IFirewallEntry ToFirewallEntry(object entry)
{
if (entry is IFirewallEntry firewallEntry)
return firewallEntry;
if (entry is IPAddress address)
return new IPFirewallEntry(address);
if (entry is string s)
return ToFirewallEntry(s);
return null;
}
public static IFirewallEntry ToFirewallEntry(string entry)
{
if (IPAddress.TryParse(entry, out IPAddress addr))
return new IPFirewallEntry(addr);
// Try CIDR parse
string[] str = entry.Split('/');
if (str.Length == 2)
if (IPAddress.TryParse(str[0], out IPAddress cidrPrefix))
if (int.TryParse(str[1], out int cidrLength))
return new CIDRFirewallEntry(cidrPrefix, cidrLength);
return new WildcardIPFirewallEntry(entry);
}
public static void RemoveAt(int index)
{
List.RemoveAt(index);
Save();
}
public static void Remove(object obj)
{
IFirewallEntry entry = ToFirewallEntry(obj);
if (entry != null)
{
List.Remove(entry);
Save();
}
}
public static void Add(object obj)
{
if (obj is IPAddress address)
Add(address);
else if (obj is string s)
Add(s);
else if (obj is IFirewallEntry entry)
Add(entry);
}
public static void Add(IFirewallEntry entry)
{
if (!List.Contains(entry))
List.Add(entry);
Save();
}
public static void Add(string pattern)
{
IFirewallEntry entry = ToFirewallEntry(pattern);
if (!List.Contains(entry))
List.Add(entry);
Save();
}
public static void Add(IPAddress ip)
{
IFirewallEntry entry = new IPFirewallEntry(ip);
if (!List.Contains(entry))
List.Add(entry);
Save();
}
public static void Save()
{
string path = "firewall.cfg";
using StreamWriter op = new StreamWriter(path);
for (int i = 0; i < List.Count; ++i)
op.WriteLine(List[i]);
}
public static bool IsBlocked(IPAddress ip)
{
for (int i = 0; i < List.Count; i++)
if (List[i].IsBlocked(ip))
return true;
return false;
/*
bool contains = false;
for ( int i = 0; !contains && i < m_Blocked.Count; ++i )
{
if (m_Blocked[i] is IPAddress)
contains = ip.Equals( m_Blocked[i] );
else if (m_Blocked[i] is String)
{
string s = (string)m_Blocked[i];
contains = Utility.IPMatchCIDR( s, ip );
if (!contains)
contains = Utility.IPMatch( s, ip );
}
}
return contains;
* */
}
public interface IFirewallEntry
{
bool IsBlocked(IPAddress address);
}
public class IPFirewallEntry : IFirewallEntry
{
private readonly IPAddress m_Address;
public IPFirewallEntry(IPAddress address) => m_Address = address;
public bool IsBlocked(IPAddress address) => m_Address.Equals(address);
public override string ToString() => m_Address.ToString();
public override bool Equals(object obj)
{
if (obj is IPAddress)
return obj.Equals(m_Address);
if (obj is string s)
{
if (IPAddress.TryParse(s, out IPAddress otherAddress))
return otherAddress.Equals(m_Address);
}
else if (obj is IPFirewallEntry entry)
{
return m_Address.Equals(entry.m_Address);
}
return false;
}
public override int GetHashCode() => m_Address.GetHashCode();
}
public class CIDRFirewallEntry : IFirewallEntry
{
private readonly int m_CIDRLength;
private readonly IPAddress m_CIDRPrefix;
public CIDRFirewallEntry(IPAddress cidrPrefix, int cidrLength)
{
m_CIDRPrefix = cidrPrefix;
m_CIDRLength = cidrLength;
}
public bool IsBlocked(IPAddress address) => Utility.IPMatchCIDR(m_CIDRPrefix, address, m_CIDRLength);
public override string ToString() => $"{m_CIDRPrefix}/{m_CIDRLength}";
public override bool Equals(object obj)
{
if (obj is string entry)
{
string[] str = entry.Split('/');
if (str.Length == 2)
if (IPAddress.TryParse(str[0], out IPAddress cidrPrefix))
if (int.TryParse(str[1], out int cidrLength))
return m_CIDRPrefix.Equals(cidrPrefix) && m_CIDRLength.Equals(cidrLength);
}
else if (obj is CIDRFirewallEntry cidrEntry)
{
return m_CIDRPrefix.Equals(cidrEntry.m_CIDRPrefix) && m_CIDRLength.Equals(cidrEntry.m_CIDRLength);
}
return false;
}
public override int GetHashCode() => m_CIDRPrefix.GetHashCode() ^ m_CIDRLength.GetHashCode();
}
public class WildcardIPFirewallEntry : IFirewallEntry
{
private readonly string m_Entry;
private bool m_Valid;
public WildcardIPFirewallEntry(string entry) => m_Entry = entry;
public bool IsBlocked(IPAddress address)
{
if (!m_Valid)
return false; // Why process if it's invalid? it'll return false anyway after processing it.
bool matched = Utility.IPMatch(m_Entry, address, out bool valid);
m_Valid = valid;
return matched;
}
public override string ToString() => m_Entry;
public override bool Equals(object obj)
{
if (obj is string)
return obj.Equals(m_Entry);
return obj is WildcardIPFirewallEntry entry && m_Entry.Equals(entry.m_Entry);
}
public override int GetHashCode() => m_Entry.GetHashCode();
}
}
}

View file

@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using Server.Network;
namespace Server.Misc
{
public class IPLimiter
{
public static bool Enabled { get; private set; }
public static bool SocketBlock { get; private set; }
public static int MaxAddresses { get; private set; }
public static void Configure()
{
Enabled = ServerConfiguration.GetOrUpdateSetting("ipLimiter.enable", true);
SocketBlock = ServerConfiguration.GetOrUpdateSetting("ipLimiter.blockAtConnection", true);
MaxAddresses = ServerConfiguration.GetOrUpdateSetting("ipLimiter.maxConnectionsPerIP", 10);
}
public static IPAddress[] Exemptions =
{
// IPAddress.Parse( "127.0.0.1" ),
};
public static bool IsExempt(IPAddress ip) => Exemptions.Contains(ip);
public static bool Verify(IPAddress ourAddress)
{
if (!Enabled || IsExempt(ourAddress))
return true;
List<NetState> netStates = TcpServer.Instances;
int count = 0;
for (int i = 0; i < netStates.Count; ++i)
{
NetState compState = netStates[i];
if (ourAddress.Equals(compState.Address))
{
++count;
if (count >= MaxAddresses)
return false;
}
}
return true;
}
}
}

View file

@ -0,0 +1,8 @@
namespace Server.Accounting
{
public interface IPasswordProtection
{
string EncryptPassword(string plainPassword);
bool ValidatePassword(string encryptedPassword, string plainPassword);
}
}

View file

@ -0,0 +1,71 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: AccountSecurity.cs *
* Created: 2020/05/01 - Updated: 2020/05/02 *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
namespace Server.Accounting.Security
{
public enum PasswordProtectionAlgorithm
{
// Obsolete algorithms from RunUO. These are not secure!
// They are included for password upgrades only.
None,
MD5,
SHA1,
// Supported algorithms
SHA2, // ServUO compatibility
PBKDF2,
Argon2 // Recommended algorithm for real security.
}
public static class AccountSecurity
{
public static PasswordProtectionAlgorithm CurrentAlgorithm { get; set; }
public static IPasswordProtection CurrentPasswordProtection => GetPasswordProtection(CurrentAlgorithm);
public static void Configure()
{
CurrentAlgorithm =
ServerConfiguration.GetOrUpdateSetting("accountSecurity.encryptionAlgorithm", PasswordProtectionAlgorithm.Argon2);
if (CurrentAlgorithm < PasswordProtectionAlgorithm.SHA2)
throw new Exception($"Security: {CurrentAlgorithm} is obsolete and not secure. Do not use it.");
}
public static IPasswordProtection GetPasswordProtection(PasswordProtectionAlgorithm algorithm)
{
var passwordProtection = algorithm switch
{
PasswordProtectionAlgorithm.MD5 => MD5PasswordProtection.Instance,
PasswordProtectionAlgorithm.SHA1 => SHA1PasswordProtection.Instance,
PasswordProtectionAlgorithm.SHA2 => SHA2PasswordProtection.Instance,
PasswordProtectionAlgorithm.PBKDF2 => PBKDF2PasswordProtection.Instance,
PasswordProtectionAlgorithm.Argon2 => Argon2PasswordProtection.Instance,
PasswordProtectionAlgorithm.None => throw new Exception("Do not use PasswordProtectionAlgorithm.None"),
_ => throw new Exception("No algorithm")
};
return passwordProtection;
}
}
}

View file

@ -0,0 +1,35 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Argon2PasswordProtection.cs *
* Created: 2020/05/01 - Updated: 2020/05/02 *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
namespace Server.Accounting.Security
{
public class Argon2PasswordProtection : IPasswordProtection
{
public static IPasswordProtection Instance = new Argon2PasswordProtection();
private Argon2PasswordHasher m_PasswordHasher = new Argon2PasswordHasher();
public string EncryptPassword(string plainPassword) =>
m_PasswordHasher.Hash(plainPassword);
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
m_PasswordHasher.Verify(encryptedPassword, plainPassword);
}
}

View file

@ -0,0 +1,46 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: MD5PasswordProtection.cs *
* Created: 2020/05/01 - Updated: 2020/05/02 *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Security.Cryptography;
using System.Text;
using Server.Misc;
namespace Server.Accounting.Security
{
public class MD5PasswordProtection : IPasswordProtection
{
public static IPasswordProtection Instance = new MD5PasswordProtection();
private MD5CryptoServiceProvider m_MD5HashProvider = new MD5CryptoServiceProvider();
public string EncryptPassword(string plainPassword)
{
ReadOnlySpan<char> password = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length));
byte[] bytes = new byte[Encoding.ASCII.GetByteCount(password)];
Encoding.ASCII.GetBytes(password, bytes);
return HexStringConverter.GetString(m_MD5HashProvider.ComputeHash(bytes));
}
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
EncryptPassword(plainPassword) == encryptedPassword;
}
}

View file

@ -0,0 +1,67 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: PBKDF2PasswordProtection.cs *
* Created: 2020/04/30 - Updated: 2020/05/01 *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Buffers.Binary;
using System.Security.Cryptography;
using Server.Misc;
namespace Server.Accounting.Security
{
public class PBKDF2PasswordProtection : IPasswordProtection
{
public static IPasswordProtection Instance = new PBKDF2PasswordProtection();
private const ushort m_MinIterations = 1024;
private const ushort m_MaxIterations = 1536;
private static readonly HashAlgorithmName m_Algorithm = HashAlgorithmName.SHA256;
private const int m_SaltSize = 8;
private const int m_HashSize = 32;
private const int m_OutputSize = 2 + m_SaltSize + m_HashSize;
public string EncryptPassword(string plainPassword)
{
Span<byte> output = stackalloc byte[m_OutputSize];
int iterations = Utility.RandomMinMax(m_MinIterations, m_MaxIterations);
BinaryPrimitives.WriteUInt16LittleEndian(output.Slice(0, 2), (ushort)iterations);
var rfc2898 = new Rfc2898DeriveBytes(plainPassword, m_SaltSize, iterations, m_Algorithm);
rfc2898.Salt.CopyTo(output.Slice(2, m_SaltSize));
rfc2898.GetBytes(m_HashSize).CopyTo(output.Slice(m_SaltSize + 2));
return HexStringConverter.GetString(output);
}
public bool ValidatePassword(string encryptedPassword, string plainPassword)
{
Span<byte> encryptedBytes = stackalloc byte[m_OutputSize];
HexStringConverter.GetBytes(encryptedPassword, encryptedBytes);
ushort iterations = BinaryPrimitives.ReadUInt16LittleEndian(encryptedBytes.Slice(0, 2));
Span<byte> salt = encryptedBytes.Slice(2, m_SaltSize);
ReadOnlySpan<byte> hash =
new Rfc2898DeriveBytes(plainPassword, salt.ToArray(), iterations, m_Algorithm).GetBytes(m_HashSize);
return hash.SequenceEqual(encryptedBytes.Slice(m_SaltSize + 2));
}
}
}

View file

@ -0,0 +1,46 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: SHA1PasswordProtection.cs *
* Created: 2020/05/01 - Updated: 2020/05/02 *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Security.Cryptography;
using System.Text;
using Server.Misc;
namespace Server.Accounting.Security
{
public class SHA1PasswordProtection : IPasswordProtection
{
public static IPasswordProtection Instance = new SHA1PasswordProtection();
private SHA1CryptoServiceProvider m_SHA1HashProvider = new SHA1CryptoServiceProvider();
public string EncryptPassword(string plainPassword)
{
ReadOnlySpan<char> password = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length));
byte[] bytes = new byte[Encoding.ASCII.GetByteCount(password)];
Encoding.ASCII.GetBytes(password, bytes);
return HexStringConverter.GetString(m_SHA1HashProvider.ComputeHash(bytes));
}
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
EncryptPassword(plainPassword) == encryptedPassword;
}
}

View file

@ -0,0 +1,46 @@
/*************************************************************************
* ModernUO *
* Copyright (C) 2019-2020 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: SHA2PasswordProtection.cs *
* Created: 2020/05/01 - Updated: 2020/05/02 *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Security.Cryptography;
using System.Text;
using Server.Misc;
namespace Server.Accounting.Security
{
public class SHA2PasswordProtection : IPasswordProtection
{
public static IPasswordProtection Instance = new SHA2PasswordProtection();
private SHA512CryptoServiceProvider m_SHA2HashProvider = new SHA512CryptoServiceProvider();
public string EncryptPassword(string plainPassword)
{
ReadOnlySpan<char> password = plainPassword.AsSpan(0, Math.Min(256, plainPassword.Length));
byte[] bytes = new byte[Encoding.ASCII.GetByteCount(password)];
Encoding.ASCII.GetBytes(password, bytes);
return HexStringConverter.GetString(m_SHA2HashProvider.ComputeHash(bytes));
}
public bool ValidatePassword(string encryptedPassword, string plainPassword) =>
EncryptPassword(plainPassword) == encryptedPassword;
}
}