/* TShock, a server mod for Terraria Copyright (C) 2011-2019 Pryaxis & TShock Contributors 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 . */ using System; using System.Linq; using System.Collections.Generic; using System.Data; using MySql.Data.MySqlClient; namespace TShockAPI.DB { /// /// Class that manages bans. /// public class BanManager { private IDbConnection database; private Dictionary _bans; /// /// Dictionary of Bans, keyed on unique ban ID /// public Dictionary Bans { get { if (_bans == null) { _bans = RetrieveAllBans().ToDictionary(b => b.UniqueId); } return _bans; } } /// /// Event invoked when a ban is checked for validity /// public static event EventHandler OnBanValidate; /// /// Event invoked before a ban is added /// public static event EventHandler OnBanPreAdd; /// /// Event invoked after a ban is added /// public static event EventHandler OnBanPostAdd; /// /// Initializes a new instance of the class. /// /// A valid connection to the TShock database public BanManager(IDbConnection db) { database = db; var table = new SqlTable("PlayerBans", new SqlColumn("Id", MySqlDbType.Int32) { Primary = true, AutoIncrement = true }, new SqlColumn("Identifier", MySqlDbType.Text), new SqlColumn("Reason", MySqlDbType.Text), new SqlColumn("BanningUser", MySqlDbType.Text), new SqlColumn("Date", MySqlDbType.Int64), new SqlColumn("Expiration", MySqlDbType.Int64) ); var creator = new SqlTableCreator(db, db.GetSqlType() == SqlType.Sqlite ? (IQueryBuilder)new SqliteQueryCreator() : new MysqlQueryCreator()); try { creator.EnsureTableStructure(table); } catch (DllNotFoundException) { System.Console.WriteLine("Possible problem with your database - is Sqlite3.dll present?"); throw new Exception("Could not find a database library (probably Sqlite3.dll)"); } OnBanValidate += BanValidateCheck; OnBanPreAdd += BanAddedCheck; } /// /// Converts bans from the old ban system to the new. /// public void ConvertBans() { using (var reader = database.QueryReader("SELECT * FROM Bans")) { while (reader.Read()) { var ip = reader.Get("IP"); var uuid = reader.Get("UUID"); var account = reader.Get("AccountName"); var reason = reader.Get("Reason"); var banningUser = reader.Get("BanningUser"); var date = reader.Get("Date"); var expiration = reader.Get("Expiration"); if (!DateTime.TryParse(date, out DateTime start)) { start = DateTime.UtcNow; } if (!DateTime.TryParse(expiration, out DateTime end)) { end = DateTime.MaxValue; } if (!string.IsNullOrWhiteSpace(ip)) { InsertBan($"{Identifiers.IP}{ip}", reason, banningUser, start, end); } if (!string.IsNullOrWhiteSpace(account)) { InsertBan($"{Identifiers.Account}{account}", reason, banningUser, start, end); } if (!string.IsNullOrWhiteSpace(uuid)) { InsertBan($"{Identifiers.UUID}{uuid}", reason, banningUser, start, end); } } } } /// /// Determines whether or not a ban is valid /// /// /// /// public bool IsValidBan(Ban ban, TSPlayer player) { BanEventArgs args = new BanEventArgs { Ban = ban, Player = player }; OnBanValidate?.Invoke(this, args); return args.Valid; } internal void BanValidateCheck(object sender, BanEventArgs args) { //Only perform validation if the event has not been cancelled before we got here if (args.Valid) { //We consider a ban to be valid if the start time is before now and the end time is after now, and the player is not immune to bans args.Valid = (DateTime.UtcNow > args.Ban.BanDateTime && DateTime.UtcNow < args.Ban.ExpirationDateTime) && !args.Player.HasPermission(Permissions.immunetoban); } } internal void BanAddedCheck(object sender, BanPreAddEventArgs args) { //Only perform validation if the event has not been cancelled before we got here if (args.Valid) { //We consider a ban valid to add if no other *current* bans exist for the identifier provided. //E.g., if a previous ban has expired, a new ban is valid. //However, if a previous ban on the provided identifier is still in effect, a new ban is not valid args.Valid = !Bans.Any(b => b.Value.Identifier == args.Identifier && b.Value.ExpirationDateTime > DateTime.UtcNow); args.Message = args.Valid ? null : "a current ban for this identifier already exists."; } } /// /// Adds a new ban for the given identifier. Returns a Ban object if the ban was added, else null /// /// /// /// /// /// /// public AddBanResult InsertBan(string identifier, string reason, string banningUser, DateTime fromDate, DateTime toDate) { BanPreAddEventArgs args = new BanPreAddEventArgs { Identifier = identifier, Reason = reason, BanningUser = banningUser, BanDateTime = fromDate, ExpirationDateTime = toDate }; OnBanPreAdd?.Invoke(this, args); if (!args.Valid) { string message = $"Ban was invalidated: {(args.Message ?? "no further information provided.")}"; return new AddBanResult { Message = message }; } string query = "INSERT INTO PlayerBans (Identifier, Reason, BanningUser, Date, Expiration) VALUES (@0, @1, @2, @3, @4);"; if (database.GetSqlType() == SqlType.Mysql) { query += "SELECT CAST(LAST_INSERT_ID() as INT);"; } else { query += "SELECT CAST(last_insert_rowid() as INT);"; } int uniqueId = database.QueryScalar(query, identifier, reason, banningUser, fromDate.Ticks, toDate.Ticks); if (uniqueId == 0) { return new AddBanResult { Message = "Database insert failed." }; } Ban b = new Ban(uniqueId, identifier, reason, banningUser, fromDate, toDate); _bans.Add(uniqueId, b); OnBanPostAdd?.Invoke(this, new BanEventArgs { Ban = b }); return new AddBanResult { Ban = b }; } /// /// Attempts to remove a ban. Returns true if the ban was removed or expired. False if the ban could not be removed or expired /// /// The unique ID of the ban to change /// If true, deletes the ban from the database. If false, marks the expiration time as now, rendering the ban expired. Defaults to false /// public bool RemoveBan(int uniqueId, bool fullDelete = false) { int rowsModified; if (fullDelete) { rowsModified = database.Query("DELETE FROM PlayerBans WHERE Id=@0", uniqueId); _bans.Remove(uniqueId); } else { rowsModified = database.Query("UPDATE PlayerBans SET Expiration=@0 WHERE Id=@1", DateTime.UtcNow.Ticks, uniqueId); _bans[uniqueId].ExpirationDateTime = DateTime.UtcNow; } return rowsModified > 0; } /// /// Retrieves a single ban from a unique ban ID /// /// /// public Ban GetBanById(int id) { if (Bans.ContainsKey(id)) { return Bans[id]; } using (var reader = database.QueryReader("SELECT * FROM PlayerBans WHERE Id=@0", id)) { if (reader.Read()) { var uniqueId = reader.Get("Id"); var identifier = reader.Get("Identifier"); var reason = reader.Get("Reason"); var banningUser = reader.Get("BanningUser"); var date = reader.Get("Date"); var expiration = reader.Get("Expiration"); return new Ban(uniqueId, identifier, reason, banningUser, date, expiration); } } return null; } /// /// Retrieves an enumerable of all bans for a given identifier /// /// Identifier to search with /// Whether or not to exclude expired bans /// public IEnumerable RetrieveBansByIdentifier(string identifier, bool currentOnly = true) { string query = "SELECT * FROM PlayerBans WHERE Identifier=@0"; if (currentOnly) { query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; } using (var reader = database.QueryReader(query, identifier)) { while (reader.Read()) { var uniqueId = reader.Get("Id"); var ident = reader.Get("Identifier"); var id = reader.Get("Identifier"); var reason = reader.Get("Reason"); var banningUser = reader.Get("BanningUser"); var date = reader.Get("Date"); var expiration = reader.Get("Expiration"); yield return new Ban(uniqueId, ident, reason, banningUser, date, expiration); } } } /// /// Retrieves an enumerable of bans for a given set of identifiers /// /// Whether or not to exclude expired bans /// /// public IEnumerable GetBansByIdentifiers(bool currentOnly = true, params string[] identifiers) { //Generate a sequence of '@0, @1, @2, ... etc' var parameters = string.Join(", ", Enumerable.Range(0, identifiers.Count()).Select(p => $"@{p}")); string query = $"SELECT * FROM PlayerBans WHERE Identifier IN ({parameters})"; if (currentOnly) { query += $" AND Expiration > {DateTime.UtcNow.Ticks}"; } using (var reader = database.QueryReader(query, identifiers)) { while (reader.Read()) { var uniqueId = reader.Get("Id"); var identifier = reader.Get("Identifier"); var reason = reader.Get("Reason"); var banningUser = reader.Get("BanningUser"); var date = reader.Get("Date"); var expiration = reader.Get("Expiration"); yield return new Ban(uniqueId, identifier, reason, banningUser, date, expiration); } } } /// /// Retrieves a list of bans from the database, sorted by their addition date from newest to oldest /// public IEnumerable RetrieveAllBans() => RetrieveAllBansSorted(BanSortMethod.AddedNewestToOldest); /// /// Retrieves an enumerable of s from the database, sorted using the provided sort method /// /// /// public IEnumerable RetrieveAllBansSorted(BanSortMethod sortMethod) { List banlist = new List(); try { var orderBy = SortToOrderByMap[sortMethod]; using (var reader = database.QueryReader($"SELECT * FROM PlayerBans ORDER BY {orderBy}")) { while (reader.Read()) { var uniqueId = reader.Get("Id"); var identifier = reader.Get("Identifier"); var reason = reader.Get("Reason"); var banningUser = reader.Get("BanningUser"); var date = reader.Get("Date"); var expiration = reader.Get("Expiration"); var ban = new Ban(uniqueId, identifier, reason, banningUser, date, expiration); banlist.Add(ban); } } } catch (Exception ex) { TShock.Log.Error(ex.ToString()); Console.WriteLine(ex.StackTrace); } return banlist; } /// /// Removes all bans from the database /// /// true, if bans were cleared, false otherwise. public bool ClearBans() { try { return database.Query("DELETE FROM PlayerBans") != 0; } catch (Exception ex) { TShock.Log.Error(ex.ToString()); } return false; } internal Dictionary SortToOrderByMap = new Dictionary { { BanSortMethod.AddedNewestToOldest, "Date DESC" }, { BanSortMethod.AddedOldestToNewest, "Date ASC" }, { BanSortMethod.ExpirationSoonestToLatest, "Expiration ASC" }, { BanSortMethod.ExpirationLatestToSoonest, "Expiration DESC" } }; } /// /// Enum containing sort options for ban retrieval /// public enum BanSortMethod { /// /// Bans will be sorted on expiration date, from soonest to latest /// ExpirationSoonestToLatest, /// /// Bans will be sorted on expiration date, from latest to soonest /// ExpirationLatestToSoonest, /// /// Bans will be sorted by the date they were added, from newest to oldest /// AddedNewestToOldest, /// /// Bans will be sorted by the date they were added, from oldest to newest /// AddedOldestToNewest, /// /// Bans will be sorted by their unique ID /// UniqueId } /// /// Result of an attempt to add a ban /// public class AddBanResult { /// /// Message generated from the attempt /// public string Message { get; set; } /// /// Ban object generated from the attempt, or null if the attempt failed /// public Ban Ban { get; set; } } /// /// Event args used for formalized bans /// public class BanEventArgs : EventArgs { /// /// Complete ban object /// public Ban Ban { get; set; } /// /// Player ban is being applied to /// public TSPlayer Player { get; set; } /// /// Whether or not the operation should be considered to be valid /// public bool Valid { get; set; } = true; } /// /// Event args used for ban data prior to a ban being formalized /// public class BanPreAddEventArgs : EventArgs { /// /// An identifiable piece of information to ban /// public string Identifier { get; set; } /// /// Gets or sets the ban reason. /// /// The ban reason. public string Reason { get; set; } /// /// Gets or sets the name of the user who added this ban entry. /// /// The banning user. public string BanningUser { get; set; } /// /// DateTime from which the ban will take effect /// public DateTime BanDateTime { get; set; } /// /// DateTime at which the ban will end /// public DateTime ExpirationDateTime { get; set; } /// /// Whether or not the operation should be considered to be valid /// public bool Valid { get; set; } = true; /// /// Optional message to explain why the event was invalidated, if it was /// public string Message { get; set; } } /// /// Contains constants for different identifier types known to TShock /// public static class Identifiers { /// /// IP identifier prefix constant /// public const string IP = "ip:"; /// /// UUID identifier prefix constant /// public const string UUID = "uuid:"; /// /// Player name identifier prefix constant /// public const string Name = "name:"; /// /// User account identifier prefix constant /// public const string Account = "acc:"; } /// /// Model class that represents a ban entry in the TShock database. /// public class Ban { /// /// A unique ID assigned to this ban /// public int UniqueId { get; set; } /// /// An identifiable piece of information to ban /// public string Identifier { get; set; } /// /// Gets or sets the ban reason. /// /// The ban reason. public string Reason { get; set; } /// /// Gets or sets the name of the user who added this ban entry. /// /// The banning user. public string BanningUser { get; set; } /// /// DateTime from which the ban will take effect /// public DateTime BanDateTime { get; set; } /// /// DateTime at which the ban will end /// public DateTime ExpirationDateTime { get; set; } /// /// Returns a string in the format dd:mm:hh:ss indicating the time until the ban expires. /// If the ban is not set to expire (ExpirationDateTime == DateTime.MaxValue), returns the string 'Never' /// /// public string GetPrettyExpirationString() { if (ExpirationDateTime == DateTime.MaxValue) { return "Never"; } TimeSpan ts = (ExpirationDateTime - DateTime.UtcNow).Duration(); // Use duration to avoid pesky negatives for expired bans return $"{ts.Days:00}:{ts.Hours:00}:{ts.Minutes:00}:{ts.Seconds:00}"; } /// /// Returns a string in the format dd:mm:hh:ss indicating the time elapsed since the ban was added. /// /// public string GetPrettyTimeSinceBanString() { TimeSpan ts = (DateTime.UtcNow - BanDateTime).Duration(); return $"{ts.Days:00}:{ts.Hours:00}:{ts.Minutes:00}:{ts.Seconds:00}"; } /// /// Initializes a new instance of the class. /// /// Unique ID assigned to the ban /// Identifier to apply the ban to /// Reason for the ban /// Account name that executed the ban /// System ticks at which the ban began /// System ticks at which the ban will end public Ban(int uniqueId, string identifier, string reason, string banningUser, long start, long end) : this(uniqueId, identifier, reason, banningUser, new DateTime(start), new DateTime(end)) { } /// /// Initializes a new instance of the class. /// /// Unique ID assigned to the ban /// Identifier to apply the ban to /// Reason for the ban /// Account name that executed the ban /// DateTime at which the ban will start /// DateTime at which the ban will end public Ban(int uniqueId, string identifier, string reason, string banningUser, DateTime start, DateTime end) { UniqueId = uniqueId; Identifier = identifier; Reason = reason; BanningUser = banningUser; BanDateTime = start; ExpirationDateTime = end; } } }