diff --git a/Server/src/main/java/core/game/content/global/report/AbuseReport.java b/Server/src/main/java/core/game/content/global/report/AbuseReport.java index 2a6c2f50b..ab7594032 100644 --- a/Server/src/main/java/core/game/content/global/report/AbuseReport.java +++ b/Server/src/main/java/core/game/content/global/report/AbuseReport.java @@ -1,6 +1,7 @@ package core.game.content.global.report; import core.game.node.entity.player.Player; +import rs09.game.system.command.CommandMapping; /** * Represents an abuse report to file. @@ -45,6 +46,9 @@ public final class AbuseReport { * @param player the player. */ public void construct(Player player, boolean mute) { + if (mute) { + CommandMapping.INSTANCE.get("mute").attemptHandling(player, new String[] {"mute", victim, "48h"}); + } player.getPacketDispatch().sendMessage("Thank-you, your abuse report has been received."); } diff --git a/Server/src/main/java/core/game/node/entity/player/Player.java b/Server/src/main/java/core/game/node/entity/player/Player.java index 60f7aefe5..2c8add884 100644 --- a/Server/src/main/java/core/game/node/entity/player/Player.java +++ b/Server/src/main/java/core/game/node/entity/player/Player.java @@ -385,6 +385,7 @@ public class Player extends Entity { CommunicationInfo.notifyPlayers(this, false, false); HouseManager.leave(this); UpdateSequence.getRenderablePlayers().remove(this); + details.save(); Repository.getDisconnectionQueue().add(this); } @@ -867,7 +868,6 @@ public class Player extends Entity { if (this.details != null) { details.setBanTime(this.details.getBanTime()); details.setMuteTime(this.details.getMuteTime()); - details.setIcon(this.details.getIcon()); } details.getSession().setObject(this); this.details = details; diff --git a/Server/src/main/java/core/game/node/entity/player/info/PlayerDetails.java b/Server/src/main/java/core/game/node/entity/player/info/PlayerDetails.java index 2ec604452..c7f763a82 100644 --- a/Server/src/main/java/core/game/node/entity/player/info/PlayerDetails.java +++ b/Server/src/main/java/core/game/node/entity/player/info/PlayerDetails.java @@ -1,9 +1,11 @@ package core.game.node.entity.player.info; import core.game.node.entity.player.info.portal.Icon; -import core.game.node.entity.player.info.portal.PlayerSQLManager; import core.game.system.communication.CommunicationInfo; import core.net.IoSession; +import org.jetbrains.annotations.NotNull; +import rs09.auth.UserAccountInfo; +import rs09.game.world.GameWorld; import java.util.concurrent.TimeUnit; @@ -13,75 +15,27 @@ import java.util.concurrent.TimeUnit; * */ public class PlayerDetails { - - /** - * The sql manager for this account. - */ - private final PlayerSQLManager sqlManager = new PlayerSQLManager(this); + public UserAccountInfo accountInfo = UserAccountInfo.createDefault(); /** * The communication info. */ private final CommunicationInfo communicationInfo = new CommunicationInfo(); + public int getCredits() { + return accountInfo.getCredits(); + } + + public void setCredits(int amount) { + accountInfo.setCredits(amount); + } + /** * The unique id info. */ private final UIDInfo info = new UIDInfo(); - /** - * The username of the account. - */ - private final String username; - - /** - * The password of the account. - */ - private String password; - - /** - * The unique id of the account. - */ - private int uid; - - /** - * The account's last game login. - */ - private long lastLogin = -1; - - /** - * The account's last game login. - */ - private long timePlayed = 0; - - /** - * The time the player is muted for. - */ - private long muteTime; - - /** - * The time the player is banned for. - */ - private long banTime; - - /** - * If the sql data has been parsed. - */ - private boolean parsed; - - /** - * The rights of the player. - */ - private Rights rights = Rights.REGULAR_PLAYER; - - /** - * The chat icon value. - */ - private Icon icon; - - public int credits; - /** * Represents the session. */ @@ -89,62 +43,10 @@ public class PlayerDetails { /** * Constructs a new {@code PlayerDetails}. - * @param username the username to set. - * @param password the password to set. - */ - public PlayerDetails(String username, String password) { - this.username = username; - this.password = password; - this.uid = username.hashCode(); - } - - /** - * Constructs a new {@Code PlayerDetails} {@Code Object} * @param username the username to set. */ public PlayerDetails(String username) { - this(username, null); - } - - /** - * Parses the details from the database for this object. - * @return {@code True} if parsed. - */ - public boolean parse() { - if (sqlManager.parse()) { - return parsed = true; - } - return parsed = false; - } - - /** - * Saves the player's details. - * @return {@code True} if saved. - */ - public boolean save() { - sqlManager.save(); - return true; - } - - /** - * Gets the player details for an account name. - * @param username the username. - * @return the player details. - */ - public static PlayerDetails getDetails(String username) { - PlayerDetails details = new PlayerDetails(username); - if (!details.parse()) { - return details = null; - } - return details; - } - - /** - * Checks if the ban is permanent. - * @return {@code True} if so. - */ - public boolean isPermBan() { - return TimeUnit.MILLISECONDS.toDays(banTime - System.currentTimeMillis()) > 1000; + accountInfo.setUsername(username); } /** @@ -152,7 +54,7 @@ public class PlayerDetails { * @return {@code True} if so. */ public boolean isBanned() { - return banTime > System.currentTimeMillis(); + return accountInfo.getBanEndTime() > System.currentTimeMillis(); } /** @@ -160,7 +62,7 @@ public class PlayerDetails { * @return {@code True} if so. */ public boolean isPermMute() { - return TimeUnit.MILLISECONDS.toDays(muteTime - System.currentTimeMillis()) > 1000; + return TimeUnit.MILLISECONDS.toDays(accountInfo.getMuteEndTime() - System.currentTimeMillis()) > 1000; } /** @@ -168,15 +70,7 @@ public class PlayerDetails { * @return {@code True} if so. */ public boolean isMuted() { - return muteTime > System.currentTimeMillis(); - } - - /** - * Gets the sql manager. - * @return the sql manager. - */ - public PlayerSQLManager getSqlManager() { - return sqlManager; + return accountInfo.getMuteEndTime() > System.currentTimeMillis(); } /** @@ -184,7 +78,7 @@ public class PlayerDetails { * @return The rights. */ public Rights getRights() { - return rights; + return Rights.values()[accountInfo.getRights()]; } /** @@ -192,7 +86,7 @@ public class PlayerDetails { * @param rights The credentials to set. */ public void setRights(Rights rights) { - this.rights = rights; + this.accountInfo.setRights(rights.ordinal()); } /** @@ -216,7 +110,7 @@ public class PlayerDetails { * @param password the password. */ public void setPassword(final String password) { - this.password = password; + this.accountInfo.setPassword(password); } /** @@ -225,7 +119,7 @@ public class PlayerDetails { */ public String getUsername() { - return username; + return this.accountInfo.getUsername(); } /** @@ -233,7 +127,7 @@ public class PlayerDetails { * @return the uid. */ public int getUid() { - return uid; + return this.accountInfo.getUid(); } /** @@ -241,7 +135,7 @@ public class PlayerDetails { * @param uid the uid. */ public void setUid(int uid) { - this.uid = uid; + } /** @@ -249,15 +143,7 @@ public class PlayerDetails { * @return The password. */ public String getPassword() { - return password; - } - - /** - * Checks if the details have been parsed. - * @return {@code True} if parsed. - */ - public boolean isParsed() { - return parsed; + return this.accountInfo.getPassword(); } /** @@ -311,28 +197,12 @@ public class PlayerDetails { return communicationInfo; } - /** - * Gets the icon. - * @return the icon. - */ - public Icon getIcon() { - return icon; - } - - /** - * Sets the icon. - * @param icon the icon to set - */ - public void setIcon(Icon icon) { - this.icon = icon; - } - /** * Gets the lastLogin. * @return the lastLogin. */ public long getLastLogin() { - return lastLogin; + return this.accountInfo.getLastLogin(); } /** @@ -340,7 +210,7 @@ public class PlayerDetails { * @param lastLogin the lastLogin to set */ public void setLastLogin(long lastLogin) { - this.lastLogin = lastLogin; + this.accountInfo.setLastLogin(lastLogin); } /** @@ -348,7 +218,7 @@ public class PlayerDetails { * @return the timePlayed. */ public long getTimePlayed() { - return timePlayed; + return this.accountInfo.getTimePlayed(); } /** @@ -356,7 +226,7 @@ public class PlayerDetails { * @param timePlayed the timePlayed to set */ public void setTimePlayed(long timePlayed) { - this.timePlayed = timePlayed; + this.accountInfo.setTimePlayed(timePlayed); } /** @@ -364,7 +234,7 @@ public class PlayerDetails { * @param muteTime the mute time. */ public void setMuteTime(long muteTime) { - this.muteTime = muteTime; + this.accountInfo.setMuteEndTime(muteTime); } /** @@ -372,7 +242,7 @@ public class PlayerDetails { * @return The mute time. */ public long getMuteTime() { - return muteTime; + return this.accountInfo.getMuteEndTime(); } /** @@ -380,7 +250,7 @@ public class PlayerDetails { * @return the banTime. */ public long getBanTime() { - return banTime; + return this.accountInfo.getBanEndTime(); } /** @@ -388,12 +258,17 @@ public class PlayerDetails { * @param banTime the banTime to set */ public void setBanTime(long banTime) { - this.banTime = banTime; + this.accountInfo.setBanEndTime(banTime); } - @Override - public String toString() { - return "PlayerDetails [sqlManager=" + sqlManager + ", communicationInfo=" + communicationInfo + ", info=" + info + ", username=" + username + ", password=" + password + ", uid=" + uid + ", lastLogin=" + lastLogin + ", muteTime=" + muteTime + ", parsed=" + parsed + ", rights=" + rights + ", icon=" + icon + ", session=" + session + "]"; + public void save() { + if(isBanned()) return; + try { + GameWorld.getAccountStorage().update(accountInfo); + } catch (IllegalStateException ignored) {} } + public static PlayerDetails getDetails(@NotNull String username) { + return new PlayerDetails(username); + } } \ No newline at end of file diff --git a/Server/src/main/java/core/game/node/entity/player/info/login/LoginConfiguration.java b/Server/src/main/java/core/game/node/entity/player/info/login/LoginConfiguration.java index 33eb9b3d3..1ed452d73 100644 --- a/Server/src/main/java/core/game/node/entity/player/info/login/LoginConfiguration.java +++ b/Server/src/main/java/core/game/node/entity/player/info/login/LoginConfiguration.java @@ -106,7 +106,7 @@ public final class LoginConfiguration { player.getPacketDispatch().sendString("Discord Invite", 378, 14); player.getPacketDispatch().sendString("Discord Invite", 378, 129); player.getPacketDispatch().sendString("Credits", 378, 94); - player.getPacketDispatch().sendString(player.getDetails().credits + "", 378, 96); + player.getPacketDispatch().sendString(player.getDetails().getCredits() + "", 378, 96); player.getPacketDispatch().sendString(" ", 378, 229); player.getPacketDispatch().sendString("Want to contribute to 2009scape?
Visit the github using the link below!", 378, 230); player.getPacketDispatch().sendString(" ", 378, 231); @@ -266,10 +266,11 @@ public final class LoginConfiguration { * @return the last login. */ public static String getLastLogin(Player player) { - String lastIp = (String) player.getDetails().getSqlManager().getTable().getColumn("lastGameIp").getValue(); - if (lastIp == null || lastIp == "") { + String lastIp = player.getDetails().accountInfo.getLastUsedIp(); + if (lastIp.equals("")) { lastIp = player.getDetails().getIpAddress(); } + player.getDetails().accountInfo.setLastUsedIp(player.getDetails().getIpAddress()); String string = "You last logged in @timeAgo from: " + lastIp; long time = player.getDetails().getLastLogin(); Date lastLogin = new Date(time); diff --git a/Server/src/main/java/core/game/node/entity/player/info/portal/PlayerSQLManager.java b/Server/src/main/java/core/game/node/entity/player/info/portal/PlayerSQLManager.java deleted file mode 100644 index 41de6532b..000000000 --- a/Server/src/main/java/core/game/node/entity/player/info/portal/PlayerSQLManager.java +++ /dev/null @@ -1,236 +0,0 @@ -package core.game.node.entity.player.info.portal; - -import core.game.node.entity.player.Player; -import core.game.node.entity.player.info.PlayerDetails; -import core.game.node.entity.player.info.Rights; -import core.game.node.entity.player.info.login.Response; -import core.game.system.SystemManager; -import core.game.system.mysql.SQLColumn; -import core.game.system.mysql.SQLEntryHandler; -import core.game.system.mysql.SQLManager; -import core.game.system.mysql.SQLTable; -import core.game.system.mysql.impl.PlayerSQLHandler; -import core.net.amsc.WorldCommunicator; -import rs09.game.ge.GrandExchangeRecords; - -import java.math.BigInteger; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; - -/** - * Manages the SQL aspect for an account. - * @author Vexia - * - */ -public final class PlayerSQLManager { - - /** - * The representation of an account sql table. - */ - private final SQLTable table = new SQLTable( - new SQLColumn("UID", Integer.class), - new SQLColumn("rights", Integer.class), - new SQLColumn("donatorType", Integer.class), - new SQLColumn("credits", Integer.class), - new SQLColumn("icon", Integer.class), - new SQLColumn("perks", String.class), - new SQLColumn("ip", String.class, false), - new SQLColumn("mac", String.class, false), - new SQLColumn("serial", String.class, false), - new SQLColumn("computerName", String.class, false), - new SQLColumn("netWorth", BigInteger.class, false), - new SQLColumn("ironManMode", String.class, false), - new SQLColumn("bank", String.class, false), - new SQLColumn("inventory", String.class, false), - new SQLColumn("equipment", String.class, false), - new SQLColumn("ge", String.class, false), - new SQLColumn("muteTime", Long.class), - new SQLColumn("banTime", Long.class), - new SQLColumn("contacts", String.class), - new SQLColumn("blocked", String.class), - new SQLColumn("clanName", String.class), - new SQLColumn("currentClan", String.class), - new SQLColumn("clanReqs", String.class), - new SQLColumn("timePlayed", Long.class), - new SQLColumn("lastLogin", Long.class), - new SQLColumn("online", Integer.class), - new SQLColumn("lastGameIp", String.class)); - - /** - * The details the SQL manager is managing. - */ - private final PlayerDetails details; - - /** - * Constructs a new {@Code PlayerSQLManager} {@Code Object} - * @param details the details. - */ - public PlayerSQLManager(PlayerDetails details) { - this.details = details; - } - - /** - * Parses the SQL table. - * @return {@code True} if parsed. - */ - @SuppressWarnings("deprecation") - public boolean parse() { - if(!SQLEntryHandler.read(new PlayerSQLHandler(details))) { - return false; - } - details.getCommunication().parse(table); - details.credits = (int) table.getColumn("credits").getValue(); - details.setBanTime((long) table.getColumn("banTime").getValue()); - details.setMuteTime((long) table.getColumn("muteTime").getValue()); - details.setIcon(Icon.forId((int) table.getColumn("icon").getValue())); - details.setRights(Rights.forId((int) table.getColumn("rights").getValue())); - details.setLastLogin(System.currentTimeMillis()); - details.setTimePlayed((long) table.getColumn("timePlayed").getValue()); - return true; - } - - /** - * Saves the changed SQL columns to the database. - */ - public void save() { - SQLEntryHandler.write(new PlayerSQLHandler(details)); - } - - /** - * Updates column values with the player instance. - * @param player The player instance. - */ - public void update(Player player) { - if (!WorldCommunicator.isEnabled()) { - details.getCommunication().save(table); - } - table.getColumn("credits").updateValue(player.getDetails().credits); - table.getColumn("bank").updateValue(player.getBank().format()); - table.getColumn("lastLogin").updateValue(player.getDetails().getLastLogin()); - table.getColumn("ge").updateValue(GrandExchangeRecords.getInstance(player).format()); - table.getColumn("inventory").updateValue(player.getInventory().format()); - table.getColumn("equipment").updateValue(player.getEquipment().format()); - table.getColumn("netWorth").updateValue(player.getMonitor().getNetworth()); - table.getColumn("lastGameIp").updateValue(player.getDetails().getIpAddress()); - table.getColumn("ironManMode").updateValue(player.getIronmanManager().getMode().name()); - table.getColumn("timePlayed").updateValue(player.getDetails().getTimePlayed() + (System.currentTimeMillis() - player.getDetails().getLastLogin())); - table.getColumn("ip").updateValue(getAddressLog((String) table.getColumn("ip").getValue(), details.getInfo().getIp())); - table.getColumn("mac").updateValue(getAddressLog((String) table.getColumn("mac").getValue(), details.getInfo().getMac())); - table.getColumn("serial").updateValue(getAddressLog((String) table.getColumn("serial").getValue(), details.getInfo().getSerial())); - table.getColumn("computerName").updateValue(getAddressLog((String) table.getColumn("computerName").getValue(), details.getInfo().getCompName())); - } - - /** - * Checks if an account was created under a name. - * @param name the name. - * @param field the field - * @return {@code True} if so. - */ - public static boolean hasSqlAccount(String name, String field) throws SQLException { - Connection connection = SQLManager.getConnection(); - if (connection == null) { - return true; - } - ResultSet result = null; - PreparedStatement statement; - statement = connection.prepareStatement("SELECT * FROM " + "members" + " WHERE " + "" + field + "='" + name.toLowerCase() + "' LIMIT 1"); - result = statement.executeQuery(); - if (result == null || !result.next()) { - SQLManager.close(connection); - return false; - } - SQLManager.close(connection); - return true; - } - - public static Response updatePassword(String name, String pass) throws SQLException{ - if (!SQLManager.isInitialized()) { - return Response.INVALID_CREDENTIALS; - } - Connection connection = SQLManager.getConnection(); - if (connection == null) { - return Response.INVALID_LOGIN_SERVER; - } - PreparedStatement statement; - statement = connection.prepareStatement("UPDATE " + "members" + " SET password='" + pass + "' WHERE " + "username" + "='" + name.toLowerCase() + "' LIMIT 1"); - boolean Success = statement.execute(); - if(Success) return Response.SUCCESSFUL; - return null; - } - - /** - * Checks if a username & password are correct. - * @param name the name. - * @param pass the pass. - * @return the response. - * @throws SQLException the exception if thrown. - */ - public static Response getCredentialResponse(String name, String pass) throws SQLException { - if (!SQLManager.isInitialized()) { - return Response.INVALID_CREDENTIALS; - } - Connection connection = SQLManager.getConnection(); - if (connection == null) { - return Response.INVALID_LOGIN_SERVER; - } - ResultSet result = null; - PreparedStatement statement; - statement = connection.prepareStatement("SELECT * FROM " + "members" + " WHERE " + "" + "username" + "='" + name.toLowerCase() + "' LIMIT 1"); - result = statement.executeQuery(); - if (result == null || !result.next()) { - SQLManager.close(connection); - return Response.INVALID_CREDENTIALS; - } - String realPass = result.getString("password"); - if (SystemManager.getEncryption().checkPassword(pass, realPass)) { - SQLManager.close(connection); - return Response.SUCCESSFUL; - } - SQLManager.close(connection); - return Response.INVALID_CREDENTIALS; - } - - /** - * Gets the address log. - * @param original the original log. - * @param address the address. - * @return the address. - */ - private String getAddressLog(String original, String address) { - String log = ""; - if (original != null && original.length() > 0) { - log += original; - if (log.charAt(log.length() - 1) != '|') { - log += "|"; - } - } - if (address != null && address.length() > 0 && (original == null || !original.contains(address))) { - log += address + "|"; - } - if (log.length() > 0 && log.charAt(log.length() - 1) == '|') { - log = log.substring(0, log.length() - 1); - } - if (log == null) { - log = ""; - } - return log; - } - - /** - * Gets the details. - * @return the details. - */ - public PlayerDetails getDetails() { - return details; - } - - /** - * Gets the table. - * @return the table. - */ - public SQLTable getTable() { - return table; - } -} diff --git a/Server/src/main/java/core/game/system/mysql/SQLEntryHandler.java b/Server/src/main/java/core/game/system/mysql/SQLEntryHandler.java deleted file mode 100644 index 3da9131de..000000000 --- a/Server/src/main/java/core/game/system/mysql/SQLEntryHandler.java +++ /dev/null @@ -1,210 +0,0 @@ -package core.game.system.mysql; - -import rs09.game.system.SystemLogger; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; - -/** - * Handles an SQL entry. - * @author Emperor - * @param The entry type. - */ -public abstract class SQLEntryHandler { - - /** - * The connection. - */ - protected Connection connection; - - /** - * The entry. - */ - protected T entry; - - /** - * The result set. - */ - protected ResultSet result; - - /** - * The table name. - */ - protected String table; - - /** - * The column name. - */ - private String column; - - /** - * The value to use. - */ - protected String value; - - /** - * Constructs a new {@code SQLEntryHandler} {@code Object}. - * @param entry The entry. - * @param table The table name. - * @param column column name. - * @param value The column value. - */ - public SQLEntryHandler(T entry, String table, String column, String value) { - this.entry = entry; - this.table = table; - this.column = column; - this.value = value; - } - - /** - * Reads the SQL entry. - * @param entry The entry. - */ - public static boolean read(SQLEntryHandler entry) { - if (!SQLManager.isInitialized()) { - return false; - } - entry.connection = entry.getConnection(); - if (entry.connection == null) { - SystemLogger.logErr("Could not read SQL data: connection is null!"); - return false; - } - boolean success = false; - try { - entry.read(); - if (entry.result == null || !entry.result.next()) { - entry.create(); - } else { - entry.parse(); - } - success = true; - } catch (SQLException e) { - e.printStackTrace(); - } - SQLManager.close(entry.connection); - entry.connection = null; - return success; - } - - /** - * Writes the entry data on the SQL database. - * @param entry The entry. - */ - public static void write(SQLEntryHandler entry) { - write(entry, entry.getConnection(), true); - } - - /** - * Reads the SQL entry. - * @param entry The entry. - */ - public static void write(SQLEntryHandler entry, Connection connection, boolean commit) { - if (connection == null) { - SystemLogger.logErr("Could not write SQL data: connection is null!"); - return; - } - entry.connection = connection; - try { - if (!commit) { - entry.connection.setAutoCommit(false); - } - entry.save(); - } catch (SQLException e) { - e.printStackTrace(); - } - if (commit) { - SQLManager.close(entry.connection); - } - entry.connection = null; - } - - /** - * Reads the table from the SQL database. - * @throws SQLException When an SQL exception occurs. - */ - public void read() throws SQLException { - PreparedStatement statement = connection.prepareStatement("SELECT " + getReadSelection() + " FROM " + table + " WHERE " + column + " = ?"); - statement.setString(1, value); - result = statement.executeQuery(); - } - - /** - * Gets the result. - * @return the result. - */ - public ResultSet getResult() { - return result; - } - - /** - * Gets the selection. - * @return The selection. - */ - public String getReadSelection() { - return "*"; - } - - /** - * Gets the writing statement. - * @param create If we are creating a new row. - * @param columns The columns to update. - */ - public PreparedStatement getWritingStatement(boolean create, String... columns) { - PreparedStatement statement = null; - try { - StringBuilder sb = new StringBuilder(); - if (create) { - sb.append("INSERT INTO ").append(table).append(" (").append(column); - for (String name : columns) { - sb.append(",").append(name); - } - sb.append(") VALUES (?"); - for (int i = 0; i < columns.length; i++) { - sb.append(",?"); - } - sb.append(")"); - } else { - sb.append("UPDATE ").append(table).append(" SET "); - for (int i = 0; i < columns.length; i++) { - sb.append(columns[i]).append("=?"); - if (i < columns.length - 1) { - sb.append(","); - } - } - sb.append(" WHERE ").append(column).append("=?"); - } - statement = connection.prepareStatement(sb.toString()); - statement.setString(create ? 1 : columns.length + 1, value); - } catch (SQLException e) { - e.printStackTrace(); - } - return statement; - } - - /** - * Parses the entry. - * @throws SQLException When an SQL exception occurs. - */ - public abstract void parse() throws SQLException; - - /** - * Creates a new row in the database. - * @throws SQLException When an SQL exception occurs. - */ - public abstract void create() throws SQLException; - - /** - * Saves the entry. - * @throws SQLException When an SQL exception occurs. - */ - public abstract void save() throws SQLException; - - /** - * Gets the connection. - * @return The connection. - */ - public abstract Connection getConnection(); - -} \ No newline at end of file diff --git a/Server/src/main/java/core/game/system/mysql/impl/HighscoreSQLHandler.java b/Server/src/main/java/core/game/system/mysql/impl/HighscoreSQLHandler.java deleted file mode 100644 index e2d48807a..000000000 --- a/Server/src/main/java/core/game/system/mysql/impl/HighscoreSQLHandler.java +++ /dev/null @@ -1,98 +0,0 @@ -package core.game.system.mysql.impl; - -import rs09.ServerConstants; -import core.game.node.entity.skill.Skills; -import core.game.node.entity.player.Player; -import core.game.node.entity.player.info.Rights; -import core.game.system.mysql.SQLEntryHandler; -import core.game.system.mysql.SQLManager; -import rs09.game.world.GameWorld; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; - -/** - * Handles the sql handler. - * @author Vexia - */ -public final class HighscoreSQLHandler extends SQLEntryHandler { - - /** - * Constructs a new {@code HighscoreSQLHandler} {@code Object}. - */ - public HighscoreSQLHandler(Player entry) { - super(entry, (SQLManager.LOCAL ? "global" : ServerConstants.DATABASE_NAME) + ".highscores", "username", entry.getName()); - } - - @Override - public void parse() throws SQLException { - - } - - @Override - public void create() throws SQLException { - if (entry.getDetails().getRights() == Rights.ADMINISTRATOR || GameWorld.getSettings().isDevMode()) { - return; - } - StringBuilder b = new StringBuilder("INSERT highscores(username,overall_xp,total_level,ironManMode,xp_0,xp_1,xp_2,xp_3,xp_4,xp_5,xp_6,xp_7,xp_8,xp_9,xp_10,xp_11,xp_12,xp_13,xp_14,xp_15,xp_16,xp_17,xp_18,xp_19,xp_20,xp_21,xp_22,xp_23) "); - b.append("VALUES('" + value + "', '" + entry.getSkills().getTotalXp() + "', '" + entry.getSkills().getTotalLevel() + "', '" + entry.getIronmanManager().getMode().name() + "', "); - int xp; - for (int i = 0; i < Skills.SKILL_NAME.length; i++) { - xp = (int) entry.getSkills().getExperience(i); - b.append("'" + xp + "'" + (i == Skills.SKILL_NAME.length - 1 ? "" : ",")); - } - b.append(")"); - PreparedStatement statement = connection.prepareStatement(b.toString()); - statement.executeUpdate(); - SQLManager.close(statement.getConnection()); - } - - @Override - public void save() throws SQLException { - if (entry.getDetails().getRights() == Rights.ADMINISTRATOR || GameWorld.getSettings().isDevMode()) { - return; - } - super.read(); - if (result == null || !result.next()) { - create(); - return; - } - if (entry.getSavedData().getActivityData().getHardcoreDeath() == true){ - //Update the SQL table to indicate the player was a Hardcore ironman that died, do not update hiscores - StringBuilder b = new StringBuilder("UPDATE highscores SET ironManMode='HARDCORE_DEAD' WHERE username ='" + value + "'"); - PreparedStatement statement = connection.prepareStatement(b.toString()); - statement.executeUpdate(); - SQLManager.close(statement.getConnection()); - return; - } - StringBuilder b = new StringBuilder("UPDATE highscores SET overall_xp='" + entry.getSkills().getTotalXp() + "', total_level='" + entry.getSkills().getTotalLevel() + "', ironManMode='" + entry.getIronmanManager().getMode().name() + "', "); - int xp; - for (int i = 0; i < Skills.SKILL_NAME.length; i++) { - xp = (int) entry.getSkills().getExperience(i); - b.append("xp_" + i + "='" + xp + "'" + (i == Skills.SKILL_NAME.length - 1 ? "" : ",")); - } - b.append("WHERE username='" + value + "'"); - PreparedStatement statement = connection.prepareStatement(b.toString()); - statement.executeUpdate(); - SQLManager.close(statement.getConnection()); - } - - /** - * Sets the data. - * @param statement the statement. - * @param startIndex the start index. - * @throws SQLException the exception. - */ - public void setData(PreparedStatement statement, int startIndex) throws SQLException { - for (int i = 0; i < Skills.SKILL_NAME.length; i++) { - statement.setInt(startIndex + i, entry.getSkills().getStaticLevel(i)); - } - } - - @Override - public Connection getConnection() { - return SQLManager.getConnection(); - } - -} \ No newline at end of file diff --git a/Server/src/main/java/core/game/system/mysql/impl/PlayerLogSQLHandler.java b/Server/src/main/java/core/game/system/mysql/impl/PlayerLogSQLHandler.java deleted file mode 100644 index d0f2a53c9..000000000 --- a/Server/src/main/java/core/game/system/mysql/impl/PlayerLogSQLHandler.java +++ /dev/null @@ -1,121 +0,0 @@ -package core.game.system.mysql.impl; - -import rs09.ServerConstants; -import core.game.system.monitor.MessageLog; -import core.game.system.monitor.PlayerMonitor; -import core.game.system.mysql.SQLEntryHandler; -import core.game.system.mysql.SQLManager; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; - -/** - * SQL Entry handler to log player's messages, duplications, & addresses. - * @author Vexia - * - */ -public class PlayerLogSQLHandler extends SQLEntryHandler { - - /** - * The column names. - */ - private static final String[] MESSAGE_COLUMNS = new String[] { "public_chat", "private_chat", "clan_chat", "address_log", "command_log", "trade_log", "ge_log", "duel_log" }; - - /** - * Constructs a new {@Code PlayerLogSQLHandler} {@Code Object} - * @param entry the player monitor entry. - */ - public PlayerLogSQLHandler(PlayerMonitor entry, String playerName) { - super(entry, (SQLManager.LOCAL ? "global" : ServerConstants.DATABASE_NAME) + ".player_logs", "username", playerName); - } - - @Override - public void parse() throws SQLException { - } - - @Override - public void create() throws SQLException { - } - - @Override - public void save() throws SQLException { - Connection connection = getConnection(); - if (connection.prepareStatement("SELECT * FROM " + table + " WHERE username='" + value + "' LIMIT 1").executeQuery().next()) { - String b = "SET "; - int size = 0; - List columns = new ArrayList<>(20); - for (int i = 0; i < MESSAGE_COLUMNS.length; i++) { - if (!entry.getLogs()[i].getMessages().isEmpty()) { - b += MESSAGE_COLUMNS[i] + " = CONCAT(" + MESSAGE_COLUMNS[i] + ", ?)" + (i == MESSAGE_COLUMNS.length - 1 ? "" : ","); - size++; - columns.add(i); - } - } - if (!entry.getDuplicationLog().getMessages().isEmpty()) { - b += (b.charAt(b.length() - 1) != ',' ? "," : "") + "duplication_log = CONCAT(duplication_log, ?)"; - } - if (b.charAt(b.length() - 1) == ',') { - b = b.substring(0, b.length() - 1); - } - PreparedStatement statement = connection.prepareStatement("UPDATE " + table + " " + b + " WHERE username='" + value + "'"); - for (int i = 0; i < size; i++) { - writeLog(statement, 1 + i, columns.get(i)); - } - if (!entry.getDuplicationLog().getMessages().isEmpty()) { - String log = ""; - MessageLog messageLog = entry.getDuplicationLog(); - for (int i = 0; i < messageLog.getMessages().size(); i++) { - if (log.contains(messageLog.getMessages().get(i))) { - continue; - } - log += messageLog.getMessages().get(i) + "\n"; - } - statement.setString(size + 1, log); - } - //statement.executeUpdate(); - } else { - PreparedStatement statement = connection.prepareStatement("INSERT INTO " + table + " (username,public_chat,private_chat,clan_chat,address_log,command_log,trade_log,ge_log,duel_log,duplication_log) VALUES(?,?,?,?,?,?,?,?,?,?)"); - statement.setString(1, value); - for (int i = 0; i < MESSAGE_COLUMNS.length; i++) { - //writeLog(statement, 2 + i, i); - } - String log = ""; - MessageLog messageLog = entry.getDuplicationLog(); - for (int i = 0; i < messageLog.getMessages().size(); i++) { - if (log.contains(messageLog.getMessages().get(i))) { - continue; - } - log += messageLog.getMessages().get(i) + "\n"; - } - statement.setString(10, log); - //statement.executeUpdate(); - } - entry.clear(); - SQLManager.close(connection); - } - - /** - * Writes a message log to the prepared statement. - * @param statement the statement. - * @param columnIndex the column index. - * @param logIndex the log index. - * @throws SQLException the exception if thrown. - */ - private void writeLog(PreparedStatement statement, int columnIndex, int logIndex) throws SQLException { - String log = ""; - MessageLog messageLog = entry.getLogs()[logIndex]; - for (int i = 0; i < messageLog.getMessages().size(); i++) { - log += messageLog.getMessages().get(i) + "\n"; - } - statement.setString(columnIndex, log); - } - - @Override - public Connection getConnection() { - return SQLManager.getConnection(); - } - -} diff --git a/Server/src/main/java/core/game/system/mysql/impl/PlayerSQLHandler.java b/Server/src/main/java/core/game/system/mysql/impl/PlayerSQLHandler.java deleted file mode 100644 index d6acf22cd..000000000 --- a/Server/src/main/java/core/game/system/mysql/impl/PlayerSQLHandler.java +++ /dev/null @@ -1,133 +0,0 @@ -package core.game.system.mysql.impl; - -import rs09.ServerConstants; -import core.game.node.entity.player.info.PlayerDetails; -import core.game.system.mysql.SQLColumn; -import core.game.system.mysql.SQLEntryHandler; -import core.game.system.mysql.SQLManager; - -import java.math.BigInteger; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.util.List; - -/** - * Handles player details parsing/saving using SQL. - * @author Emperor - */ -public final class PlayerSQLHandler extends SQLEntryHandler { - - /** - * Constructs a new {@code PlayerSQLHandler} {@code Object}. - * @param entry The player details. - */ - public PlayerSQLHandler(PlayerDetails entry) { - super(entry, (SQLManager.LOCAL ? "global" : ServerConstants.DATABASE_NAME) + ".members", "username", entry.getUsername()); - } - - @Override - public void parse() throws SQLException { - for (SQLColumn column : entry.getSqlManager().getTable().getColumns()) { - if (!column.isParse()) { - continue; - } - if (column.getType() == Integer.class) { - column.setValue(result.getInt(column.getName())); - } else if (column.getType() == String.class) { - column.setValue(result.getString(column.getName())); - } else if (column.getType() == Boolean.class) { - column.setValue(result.getBoolean(column.getName())); - } else if (column.getType() == Long.class) { - column.setValue(result.getLong(column.getName())); - } else if (column.getType() == Timestamp.class) { - column.setValue(result.getTimestamp(column.getName())); - } else if (column.getType() == BigInteger.class) { - column.setValue(result.getLong(column.getName())); - } - } - } - - @Override - public String getReadSelection() { - StringBuilder selection = new StringBuilder(); - boolean first = true; - for (SQLColumn column : entry.getSqlManager().getTable().getColumns()) { - if (!first) { - selection.append(","); - } else { - first = false; - } - selection.append(column.getName()); - } - return selection.toString(); - } - - @Override - public void create() throws SQLException { - if (entry == null) { - return; - } - String[] names = new String[entry.getSqlManager().getTable().getColumns().length]; - for (int i = 0; i < entry.getSqlManager().getTable().getColumns().length; i++) { - names[i] = entry.getSqlManager().getTable().getColumns()[i].getName(); - } - PreparedStatement statement = getWritingStatement(true, names); - for (int i = 0; i < entry.getSqlManager().getTable().getColumns().length; i++) { - SQLColumn column = entry.getSqlManager().getTable().getColumns()[i]; - if (column.getType() == Integer.class) { - statement.setInt(i + 2, (int) column.getValue()); - } else if (column.getType() == String.class) { - statement.setString(i + 2, (String) column.getValue()); - } else if (column.getType() == Boolean.class) { - statement.setBoolean(i + 2, (boolean) column.getValue()); - } else if (column.getType() == Long.class) { - statement.setLong(i + 2, (Long) column.getValue()); - } else if (column.getType() == Timestamp.class) { - statement.setTimestamp(i + 2, (Timestamp) column.getValue()); - } else if (column.getType() == java.math.BigInteger.class) { - statement.setLong(i + 2, (Long) column.getValue()); - } - } - statement.executeUpdate(); - SQLManager.close(statement.getConnection()); - } - - @Override - public void save() throws SQLException { - List updated = entry.getSqlManager().getTable().getChanged(); - if (updated.isEmpty()) { - return; - } - String[] names = new String[updated.size()]; - for (int i = 0; i < names.length; i++) { - names[i] = updated.get(i).getName(); - } - PreparedStatement statement = getWritingStatement(false, names); - for (int i = 0; i < updated.size(); i++) { - SQLColumn column = updated.get(i); - if (column.getType() == Integer.class) { - statement.setInt(i + 1, (int) column.getValue()); - } else if (column.getType() == String.class) { - statement.setString(i + 1, (String) column.getValue()); - } else if (column.getType() == Boolean.class) { - statement.setBoolean(i + 1, (boolean) column.getValue()); - } else if (column.getType() == Long.class) { - statement.setLong(i + 1, (Long) column.getValue()); - } else if (column.getType() == Timestamp.class) { - statement.setTimestamp(i + 1, (Timestamp) column.getValue()); - } else if (column.getType() == BigInteger.class) { - statement.setLong(i + 1, (Long) column.getValue()); - } - } - statement.executeUpdate(); - SQLManager.close(statement.getConnection()); - } - - @Override - public Connection getConnection() { - return SQLManager.getConnection(); - } - -} \ No newline at end of file diff --git a/Server/src/main/java/core/net/IoSession.java b/Server/src/main/java/core/net/IoSession.java index f3b8d5f1f..1f2e74078 100644 --- a/Server/src/main/java/core/net/IoSession.java +++ b/Server/src/main/java/core/net/IoSession.java @@ -5,6 +5,7 @@ import core.game.node.entity.player.Player; import core.game.node.entity.player.info.ClientInfo; import core.game.node.entity.player.info.login.Response; import core.game.system.task.Pulse; +import rs09.auth.AuthResponse; import rs09.game.world.GameWorld; import core.net.producer.HSEventProducer; import core.net.producer.LoginEventProducer; @@ -142,7 +143,7 @@ public class IoSession { if (context == null) { throw new IllegalStateException("Invalid writing context!"); } - if (!(context instanceof Response) && producer instanceof LoginEventProducer) { + if (!(context instanceof AuthResponse) && producer instanceof LoginEventProducer) { // new Throwable().printStackTrace(); return; } diff --git a/Server/src/main/java/core/net/amsc/MSPacketRepository.java b/Server/src/main/java/core/net/amsc/MSPacketRepository.java index 0917a4281..8e7ca3a4f 100644 --- a/Server/src/main/java/core/net/amsc/MSPacketRepository.java +++ b/Server/src/main/java/core/net/amsc/MSPacketRepository.java @@ -16,6 +16,7 @@ import core.net.packet.context.MessageContext; import core.net.packet.out.CommunicationMessage; import core.net.packet.out.ContactPackets; import core.net.packet.out.UpdateClanChat; +import rs09.auth.AuthResponse; import rs09.game.node.entity.player.info.login.LoginParser; import rs09.game.system.SystemLogger; import rs09.game.world.GameWorld; @@ -306,17 +307,17 @@ public final class MSPacketRepository { LoginParser parser = WorldCommunicator.finishLoginAttempt(username); if (parser != null) { PlayerDetails details = parser.getDetails(); - Response response = Response.get(opcode); + AuthResponse response = AuthResponse.values()[opcode]; Player player = null; switch (response) { - case ALREADY_ONLINE: + case AlreadyOnline: player = Repository.getPlayerByName(username); if (player == null || player.getSession().isActive() || !player.getSession().getAddress().equals(details.getSession().getAddress())) { details.getSession().write(response, true); break; } player.getPacketDispatch().sendLogout(); - case SUCCESSFUL: + case Success: if (!details.getSession().isActive()) { sendPlayerRemoval(username); break; @@ -326,10 +327,10 @@ public final class MSPacketRepository { } else { player.updateDetails(details); } - parser.initialize(player, response == Response.ALREADY_ONLINE); + parser.initialize(player, response == AuthResponse.AlreadyOnline); break; - case MOVING_WORLD: + case MovingWorld: details.getSession().setServerKey(buffer.get()); default: details.getSession().write(response, true); diff --git a/Server/src/main/java/core/net/amsc/WorldCommunicator.java b/Server/src/main/java/core/net/amsc/WorldCommunicator.java index cce4f911e..56a5f4fdb 100644 --- a/Server/src/main/java/core/net/amsc/WorldCommunicator.java +++ b/Server/src/main/java/core/net/amsc/WorldCommunicator.java @@ -8,6 +8,7 @@ import core.net.IoSession; import core.net.NioReactor; import core.net.producer.MSHSEventProducer; import kotlin.Unit; +import rs09.auth.AuthResponse; import rs09.game.node.entity.player.info.login.LoginParser; import rs09.game.system.SystemLogger; import rs09.game.world.GameWorld; @@ -73,15 +74,11 @@ public final class WorldCommunicator { public static void register(final LoginParser parser) { LoginParser p = loginAttempts.get(parser.getDetails().getUsername()); if (p != null && GameWorld.getTicks() - p.getTimeStamp() < 50 && p.getDetails().getRights() == Rights.REGULAR_PLAYER) { - parser.getDetails().getSession().write(Response.ALREADY_ONLINE, true); + parser.getDetails().getSession().write(AuthResponse.AlreadyOnline, true); return; } loginAttempts.put(parser.getDetails().getUsername(), parser); TaskExecutor.executeSQL(() -> { - if (!parser.getDetails().parse()) { - parser.getDetails().getSession().write(Response.INVALID_LOGIN_SERVER, true); - return Unit.INSTANCE; - } MSPacketRepository.sendPlayerRegistry(parser); return Unit.INSTANCE; }); diff --git a/Server/src/main/java/core/net/event/LoginWriteEvent.java b/Server/src/main/java/core/net/event/LoginWriteEvent.java index 95b4e61c8..793477da9 100644 --- a/Server/src/main/java/core/net/event/LoginWriteEvent.java +++ b/Server/src/main/java/core/net/event/LoginWriteEvent.java @@ -6,6 +6,7 @@ import core.net.EventProducer; import core.net.IoSession; import core.net.IoWriteEvent; import core.net.producer.GameEventProducer; +import rs09.auth.AuthResponse; import java.nio.ByteBuffer; @@ -31,10 +32,10 @@ public final class LoginWriteEvent extends IoWriteEvent { @Override public void write(IoSession session, Object context) { - Response response = (Response) context; + AuthResponse response = (AuthResponse) context; ByteBuffer buffer = ByteBuffer.allocate(500); - buffer.put((byte) response.opcode()); - switch (response.opcode()) { + buffer.put((byte) response.ordinal()); + switch (response.ordinal()) { case 2: //successful login buffer.put(getWorldResponse(session)); session.setProducer(GAME_PRODUCER); diff --git a/Server/src/main/java/core/net/packet/in/ReportAbusePacket.java b/Server/src/main/java/core/net/packet/in/ReportAbusePacket.java index 466f65e15..b13f9544b 100644 --- a/Server/src/main/java/core/net/packet/in/ReportAbusePacket.java +++ b/Server/src/main/java/core/net/packet/in/ReportAbusePacket.java @@ -7,6 +7,7 @@ import core.game.node.entity.player.Player; import core.net.packet.IncomingPacket; import core.net.packet.IoBuffer; import core.tools.StringUtils; +import rs09.game.world.GameWorld; import java.io.File; @@ -21,8 +22,7 @@ public class ReportAbusePacket implements IncomingPacket { String target = StringUtils.longToString(buffer.getLong()); Rule rule = Rule.forId(buffer.get()); boolean mute = buffer.get() == 1; - File file = new File(ServerConstants.PLAYER_SAVE_PATH + target + ".save"); - if (!file.exists()) { + if (!GameWorld.getAccountStorage().checkUsernameTaken(target.toLowerCase())) { player.getPacketDispatch().sendMessage("Invalid player name."); return; } diff --git a/Server/src/main/java/core/net/registry/AccountRegister.java b/Server/src/main/java/core/net/registry/AccountRegister.java index 7fc98a9a5..4497da838 100644 --- a/Server/src/main/java/core/net/registry/AccountRegister.java +++ b/Server/src/main/java/core/net/registry/AccountRegister.java @@ -1,21 +1,17 @@ package core.net.registry; import core.cache.misc.buffer.ByteBufferUtils; -import core.game.node.entity.player.info.portal.PlayerSQLManager; -import core.game.system.SystemManager; -import core.game.system.mysql.SQLEntryHandler; -import core.game.system.mysql.SQLManager; -import core.game.system.task.TaskExecutor; +import core.game.system.task.Pulse; import core.net.Constants; import core.net.IoSession; -import kotlin.Unit; import rs09.ServerConstants; +import rs09.auth.UserAccountInfo; import rs09.game.system.SystemLogger; import rs09.game.world.GameWorld; import rs09.net.event.LoginReadEvent; +import rs09.net.packet.in.Login; import java.nio.ByteBuffer; -import java.sql.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -24,31 +20,12 @@ import java.util.regex.Pattern; * @author Vexia * */ -public class AccountRegister extends SQLEntryHandler { - - /** - * The table name. - */ - private static final String TABLE = "members"; - - /** - * The column name. - */ - private static final String COLUMN = "username"; - +public class AccountRegister { /** * The pattern compiler. */ private static final Pattern PATTERN = Pattern.compile("[a-z0-9_]{1,12}"); - /** - * Constructs a new {@Code AccountRegister} {@Code Object} - * @param entry The registry entry. - */ - public AccountRegister(RegistryDetails entry) { - super(entry, TABLE, COLUMN, entry.getUsername()); - } - /** * Reads the incoming opcode of an account register. * @param session the session. @@ -57,6 +34,7 @@ public class AccountRegister extends SQLEntryHandler { */ public static void read(final IoSession session, int opcode, ByteBuffer buffer) { int day,month,year,country; + UserAccountInfo info = UserAccountInfo.createDefault(); switch (opcode) { case 147://details day = buffer.get(); @@ -67,33 +45,29 @@ public class AccountRegister extends SQLEntryHandler { break; case 186://username final String username = ByteBufferUtils.getString(buffer).replace(" ", "_").toLowerCase().replace("|", ""); + info.setUsername(username); if (username.length() <= 0 || username.length() > 12) { response(session, RegistryResponse.INVALID_USERNAME); break; } - if (!validUsername(username)) { + if (invalidUsername(username)) { System.out.println("AHAHHA " + username); response(session,RegistryResponse.INVALID_USERNAME); break; } System.out.println(username); - TaskExecutor.executeSQL(() -> { - try { - if (PlayerSQLManager.hasSqlAccount(username, "username")) { - response(session, RegistryResponse.NOT_AVAILBLE_USER); - return Unit.INSTANCE; - } - response(session, RegistryResponse.SUCCESS); - } catch (SQLException e) { - e.printStackTrace(); - } - return Unit.INSTANCE; - }); + if (!GameWorld.getAuthenticator().canCreateAccountWith(info)) { + response(session, RegistryResponse.NOT_AVAILBLE_USER); + return; + } + response(session, RegistryResponse.SUCCESS); break; case 36://Register details + SystemLogger.logInfo("Made it to final stage"); buffer.get(); //Useless size being written that is already written in the RSA block - buffer = LoginReadEvent.getRSABlock(buffer); + buffer = Login.decryptRSABuffer(buffer, ServerConstants.EXPONENT, ServerConstants.MODULUS); if(buffer.get() != 10){ //RSA header (aka did this decrypt properly) + SystemLogger.logInfo("Decryption failed during registration :("); response(session, RegistryResponse.CANNOT_CREATE); break; } @@ -106,6 +80,8 @@ public class AccountRegister extends SQLEntryHandler { final String name = ByteBufferUtils.getString(buffer).replace(" ", "_").toLowerCase().replace("|", ""); buffer.getInt(); String password = ByteBufferUtils.getString(buffer); + info.setUsername(name); + info.setPassword(password); if (password.length() < 5 || password.length() > 20) { response(session, RegistryResponse.INVALID_PASS_LENGTH); break; @@ -114,7 +90,7 @@ public class AccountRegister extends SQLEntryHandler { response(session, RegistryResponse.PASS_SIMILAR_TO_USER); break; } - if (!validUsername(name)) { + if (invalidUsername(name)) { response(session, RegistryResponse.INVALID_USERNAME); break; } @@ -126,21 +102,17 @@ public class AccountRegister extends SQLEntryHandler { year = buffer.getShort(); country = buffer.getShort(); buffer.getInt(); - @SuppressWarnings("deprecation") - final RegistryDetails details = new RegistryDetails(name, SystemManager.getEncryption().hashPassword(password), new Date(year, month, day), country); - TaskExecutor.execute(() -> { - try { - if (PlayerSQLManager.hasSqlAccount(name, "username")) { - response(session, RegistryResponse.CANNOT_CREATE); - return Unit.INSTANCE; - } - SQLEntryHandler.write(new AccountRegister(details)); + if (!GameWorld.getAuthenticator().canCreateAccountWith(info)) { + response(session, RegistryResponse.CANNOT_CREATE); + return; + } + GameWorld.getAuthenticator().createAccountWith(info); + GameWorld.getPulser().submit(new Pulse() { + @Override + public boolean pulse() { response(session, RegistryResponse.SUCCESS); - } catch (SQLException e) { - e.printStackTrace(); - response(session, RegistryResponse.CANNOT_CREATE); + return true; } - return Unit.INSTANCE; }); break; default: @@ -149,28 +121,6 @@ public class AccountRegister extends SQLEntryHandler { } } - @Override - public void create() throws SQLException {} - - @Override - public void parse() throws SQLException {} - - @Override - public void save() throws SQLException { - PreparedStatement statement = getWritingStatement(true, "password", "salt", "birthday", "countryCode", "joined_date","currentClan"); - statement.setString(1, entry.getUsername()); - statement.setString(2, entry.getPassword()); - statement.setString(3, entry.getPassword().substring(0, 29)); - statement.setDate(4, entry.getBirth()); - statement.setInt(5, entry.getCountry()); - statement.setTimestamp(6, new Timestamp(System.currentTimeMillis())); - - statement.setString(7, GameWorld.getSettings().getEnable_default_clan() ? ServerConstants.SERVER_NAME.toLowerCase(): null); - - statement.executeUpdate(); - SQLManager.close(statement.getConnection()); - } - /** * Sends a registry response code. * @param response the response. @@ -178,22 +128,15 @@ public class AccountRegister extends SQLEntryHandler { private static void response(IoSession session, RegistryResponse response) { ByteBuffer buf = ByteBuffer.allocate(100); buf.put((byte) response.getId()); - session.queue((ByteBuffer) buf.flip()); + session.queue(buf.flip()); } /** * Checks if a username is valid. * @return {@code True} if so. */ - public static boolean validUsername(final String username) { + public static boolean invalidUsername(final String username) { Matcher matcher = PATTERN.matcher(username); - return matcher.matches(); - + return !matcher.matches(); } - - @Override - public Connection getConnection() { - return SQLManager.getConnection(); - } - } diff --git a/Server/src/main/java/core/tools/PlayerLoader.java b/Server/src/main/java/core/tools/PlayerLoader.java index baebf05a9..ee37c855a 100644 --- a/Server/src/main/java/core/tools/PlayerLoader.java +++ b/Server/src/main/java/core/tools/PlayerLoader.java @@ -19,8 +19,8 @@ public final class PlayerLoader { * @return the player. */ public static Player getPlayerFile(String name) { - final PlayerDetails playerDetails = new PlayerDetails(name, ""); - playerDetails.parse(); + final PlayerDetails playerDetails = new PlayerDetails(name); + //playerDetails.parse(); final Player player = new Player(playerDetails); PlayerParser.parse(player); // GameWorld.getWorld().getAccountService().loadPlayer(player); @@ -34,8 +34,8 @@ public final class PlayerLoader { * @return the details */ public static PlayerDetails getPlayerDetailFile(String name) { - final PlayerDetails playerDetails = new PlayerDetails(name, ""); - playerDetails.parse(); + final PlayerDetails playerDetails = new PlayerDetails(name); + // playerDetails.parse(); return playerDetails; } } diff --git a/Server/src/main/java/core/tools/mysql/DatabaseManager.java b/Server/src/main/java/core/tools/mysql/DatabaseManager.java deleted file mode 100644 index a3210ccaa..000000000 --- a/Server/src/main/java/core/tools/mysql/DatabaseManager.java +++ /dev/null @@ -1,99 +0,0 @@ -package core.tools.mysql; - -import java.sql.Connection; -import java.sql.DriverManager; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.HashMap; -import java.util.Map; - -import rs09.ServerConstants; -import rs09.game.system.SystemLogger; - -public class DatabaseManager { - - private Map connections = new HashMap<>(); - private Map databases; - - private Database db; - private boolean connected; - - public DatabaseManager(Database db) { - this.db = db; - } - - public DatabaseManager connect() { - try { - Class.forName("com.mysql.cj.jdbc.Driver"); - Connection connection = DriverManager.getConnection("jdbc:mysql://" + db.host() + "/" + db.name() + "?useTimezone=true&serverTimezone=UTC", db.username(), db.password()); - connections.put(db.name(), connection); - - SystemLogger.logInfo("Successfully connected with '" + db.name() + "'."); - - this.connected = true; - - - } catch (SQLException e) { - SystemLogger.logErr("Couldn't connect to the database."); - e.printStackTrace(); - ServerConstants.MYSQL = false; - } catch (ClassNotFoundException e) { - e.printStackTrace(); - } - return this; - } - - public ResultSet query(String name, String query) { - - try { - - Connection connection = connections().get(name); - - if (connection == null) - return null; - - Statement statement = connection.createStatement(); - - return statement.executeQuery(query); - - } catch (SQLException e) { - e.printStackTrace(); - } - - return null; - } - - public int update(String name, String query) { - - try { - - Connection connection = connections().get(name); - - if (connection == null) - return -1; - - Statement statement = connection.createStatement(); - - return statement.executeUpdate(query); - - } catch (SQLException e) { - e.printStackTrace(); - } - - return -1; - } - - public Map connections() { - return connections; - } - - public Map databases() { - return databases; - } - - public boolean isConnected() { - return connected; - } - -} diff --git a/Server/src/main/java/rs09/game/ai/resource/ResourceAIPManager.java b/Server/src/main/java/rs09/game/ai/resource/ResourceAIPManager.java index 3bc015317..0b62143ad 100644 --- a/Server/src/main/java/rs09/game/ai/resource/ResourceAIPManager.java +++ b/Server/src/main/java/rs09/game/ai/resource/ResourceAIPManager.java @@ -57,21 +57,6 @@ public class ResourceAIPManager { } public ResourceAIPManager load(Player player) { - try { - Statement statement = GameWorld.getDatabaseManager().connections().get("global").createStatement(); - - ResultSet result = statement.executeQuery("SELECT * FROM `members` WHERE username='" + player.getUsername() + "'"); - // Results result = new Results(GameWorld.getDatabaseManager().query("global", "SELECT * FROM `members` WHERE username='" + player.getUsername() + "'")); - - while (result.next()) { - String eventName = result.getString("taskName"); - String eventTime = result.getString("taskTime"); - reActivate(eventName, Long.valueOf(eventTime)); - } - - } catch (Exception e) { - e.printStackTrace(); - } return this; } @@ -87,7 +72,6 @@ public class ResourceAIPManager { StringBuilder query = new StringBuilder(); query.append("UPDATE `members` SET `taskName`='" + entry.getKey().getTaskName() + "',`taskTime`='" + entry.getValue() + "' WHERE `username`='" + player.getUsername() + "'"); System.out.println("ResourceAIPManager: " + query.toString()); - GameWorld.getDatabaseManager().update("global", query.toString()); } return this; diff --git a/Server/src/main/kotlin/rs09/auth/AuthProvider.kt b/Server/src/main/kotlin/rs09/auth/AuthProvider.kt new file mode 100644 index 000000000..5d6203d63 --- /dev/null +++ b/Server/src/main/kotlin/rs09/auth/AuthProvider.kt @@ -0,0 +1,22 @@ +package rs09.auth + +import core.game.node.entity.player.Player +import rs09.storage.AccountStorageProvider + +abstract class AuthProvider { + lateinit var storageProvider: T + + abstract fun configureFor(provider: T) + + fun canCreateAccountWith(info: UserAccountInfo) : Boolean { + return !storageProvider.checkUsernameTaken(info.username) + } + + abstract fun createAccountWith(info: UserAccountInfo) : Boolean + + abstract fun checkLogin(username: String, password: String) : Pair + + abstract fun checkPassword(player: Player, password: String) : Boolean + + abstract fun updatePassword(username: String, newPassword: String) +} diff --git a/Server/src/main/kotlin/rs09/auth/AuthResponse.kt b/Server/src/main/kotlin/rs09/auth/AuthResponse.kt new file mode 100644 index 000000000..ef1c7393c --- /dev/null +++ b/Server/src/main/kotlin/rs09/auth/AuthResponse.kt @@ -0,0 +1,27 @@ +package rs09.auth + +enum class AuthResponse { + UnexpectedError, + CouldNotAd, + Success, + InvalidCredentials, + AccountDisabled, + AlreadyOnline, + Updated, + FullWorld, + LoginServerOffline, + LoginLimitExceeded, + BadSessionID, + WeakPassword, + MembersWorld, + CouldNotLogin, + Updating, + TooManyIncorrectLogins, + StandingInMembersArea, + AccountLocked, + ClosedBeta, + InvalidLoginServer, + MovingWorld, + ErrorLoadingProfile, + BannedUser +} diff --git a/Server/src/main/kotlin/rs09/auth/DevelopmentAuthenticator.kt b/Server/src/main/kotlin/rs09/auth/DevelopmentAuthenticator.kt new file mode 100644 index 000000000..7b1e283e8 --- /dev/null +++ b/Server/src/main/kotlin/rs09/auth/DevelopmentAuthenticator.kt @@ -0,0 +1,39 @@ +package rs09.auth + +import core.game.node.entity.player.Player +import rs09.storage.InMemoryStorageProvider + +class DevelopmentAuthenticator : AuthProvider() { + override fun configureFor(provider: InMemoryStorageProvider) { + storageProvider = provider + } + + override fun checkLogin(username: String, password: String): Pair { + val info: UserAccountInfo + if(!storageProvider.checkUsernameTaken(username.toLowerCase())) { + info = UserAccountInfo.createDefault() + info.username = username + createAccountWith(info) + } else { + info = storageProvider.getAccountInfo(username.toLowerCase()) + } + return Pair(AuthResponse.Success, storageProvider.getAccountInfo(username)) + } + + override fun createAccountWith(info: UserAccountInfo): Boolean { + info.username = info.username.toLowerCase() + info.rights = 2 + storageProvider.store(info) + return true + } + + override fun checkPassword(player: Player, password: String): Boolean { + return password == player.details.password + } + + override fun updatePassword(username: String, newPassword: String) { + val info = storageProvider.getAccountInfo(username) + info.password = newPassword + storageProvider.update(info) + } +} diff --git a/Server/src/main/kotlin/rs09/auth/ProductionAuthenticator.kt b/Server/src/main/kotlin/rs09/auth/ProductionAuthenticator.kt new file mode 100644 index 000000000..424fe5357 --- /dev/null +++ b/Server/src/main/kotlin/rs09/auth/ProductionAuthenticator.kt @@ -0,0 +1,61 @@ +package rs09.auth + +import core.game.node.entity.player.Player +import core.game.system.SystemManager +import rs09.ServerConstants +import rs09.storage.AccountStorageProvider +import rs09.storage.SQLStorageProvider +import java.sql.SQLDataException + +class ProductionAuthenticator : AuthProvider() { + override fun configureFor(provider: AccountStorageProvider) { + storageProvider = provider + if (provider is SQLStorageProvider) { + provider.configure(ServerConstants.DATABASE_ADDRESS!!, ServerConstants.DATABASE_NAME!!, ServerConstants.DATABASE_USER!!, ServerConstants.DATABASE_PASS!!) + } + } + + override fun createAccountWith(info: UserAccountInfo): Boolean { + try { + info.password = SystemManager.getEncryption().hashPassword(info.password) + storageProvider.store(info) + } catch (e: SQLDataException) { + return false + } catch (e: Exception) { + e.printStackTrace() + return false + } + return true + } + + override fun checkLogin(username: String, password: String): Pair { + val info: UserAccountInfo + try { + if (!storageProvider.checkUsernameTaken(username.toLowerCase())) { + return Pair(AuthResponse.InvalidCredentials, null) + } + info = storageProvider.getAccountInfo(username.toLowerCase()) + val passCorrect = SystemManager.getEncryption().checkPassword(password, info.password) + if(!passCorrect || info.password.isEmpty()) + return Pair(AuthResponse.InvalidCredentials, null) + if(info.banEndTime > System.currentTimeMillis()) + return Pair(AuthResponse.AccountDisabled, null) + if(info.online) + return Pair(AuthResponse.AlreadyOnline, null) + } catch (e: Exception) { + e.printStackTrace() + return Pair(AuthResponse.CouldNotLogin, null) + } + return Pair(AuthResponse.Success, info) + } + + override fun checkPassword(player: Player, password: String): Boolean { + return SystemManager.getEncryption().checkPassword(password, player.details.password) + } + + override fun updatePassword(username: String, newPassword: String) { + val info = storageProvider.getAccountInfo(username) + info.password = SystemManager.getEncryption().hashPassword(newPassword) + storageProvider.update(info) + } +} \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/auth/UserAccountInfo.kt b/Server/src/main/kotlin/rs09/auth/UserAccountInfo.kt new file mode 100644 index 000000000..f16637b79 --- /dev/null +++ b/Server/src/main/kotlin/rs09/auth/UserAccountInfo.kt @@ -0,0 +1,81 @@ +package rs09.auth + +class UserAccountInfo( + var username: String, + var password: String, + var uid: Int, + var rights: Int, + var credits: Int, + var ip: String, + var lastUsedIp: String, + var muteEndTime: Long, + var banEndTime: Long, + var contacts: String, + var blocked: String, + var clanName: String, + var currentClan: String, + var clanReqs: String, + var timePlayed: Long, + var lastLogin: Long, + var online: Boolean +) { + companion object { + @JvmStatic fun createDefault() : UserAccountInfo { + return UserAccountInfo("", "", 0, 0, 0, "", "", 0L, 0L, "", "", "", "", "1,0,8,9", 0L, 0L, false) + } + } + + override fun toString(): String { + return "USER:$username,PASS:$password,UID:$uid,RIGHTS:$rights,CREDITS:$credits,IP:$ip,LASTIP:$lastUsedIp" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as UserAccountInfo + + if (username != other.username) return false + if (password != other.password) return false + if (uid != other.uid) return false + if (rights != other.rights) return false + if (credits != other.credits) return false + if (ip != other.ip) return false + if (lastUsedIp != other.lastUsedIp) return false + if (muteEndTime != other.muteEndTime) return false + if (banEndTime != other.banEndTime) return false + if (contacts != other.contacts) return false + if (blocked != other.blocked) return false + if (clanName != other.clanName) return false + if (currentClan != other.currentClan) return false + if (clanReqs != other.clanReqs) return false + if (timePlayed != other.timePlayed) return false + if (lastLogin != other.lastLogin) return false + if (online != other.online) return false + + return true + } + + override fun hashCode(): Int { + var result = username.hashCode() + result = 31 * result + password.hashCode() + result = 31 * result + uid + result = 31 * result + rights + result = 31 * result + credits + result = 31 * result + ip.hashCode() + result = 31 * result + lastUsedIp.hashCode() + result = 31 * result + muteEndTime.hashCode() + result = 31 * result + banEndTime.hashCode() + result = 31 * result + contacts.hashCode() + result = 31 * result + blocked.hashCode() + result = 31 * result + clanName.hashCode() + result = 31 * result + currentClan.hashCode() + result = 31 * result + clanReqs.hashCode() + result = 31 * result + timePlayed.hashCode() + result = 31 * result + lastLogin.hashCode() + result = 31 * result + online.hashCode() + return result + } + + +} \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/content/global/worldevents/holiday/easter/EasterBunnyDialogueFile.kt b/Server/src/main/kotlin/rs09/game/content/global/worldevents/holiday/easter/EasterBunnyDialogueFile.kt index 199048611..b8a43cead 100644 --- a/Server/src/main/kotlin/rs09/game/content/global/worldevents/holiday/easter/EasterBunnyDialogueFile.kt +++ b/Server/src/main/kotlin/rs09/game/content/global/worldevents/holiday/easter/EasterBunnyDialogueFile.kt @@ -60,7 +60,7 @@ class EasterBunnyDialogueFile(val NEED_BASKET : Boolean) : DialogueFile() { player!!.dialogueInterpreter.sendDialogue("You need 5 eggs to afford that.") } else { player!!.incrementAttribute(EGG_ATTRIBUTE, -5) - player!!.details.credits += 1 + player!!.details.accountInfo.credits += 1 player!!.dialogueInterpreter.sendDialogue( "You turn in 5 eggs in exchange for a credit.", "You now have ${player!!.getAttribute(EGG_ATTRIBUTE, 0)} eggs." diff --git a/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt b/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt index c048c16ce..f0df87236 100644 --- a/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt +++ b/Server/src/main/kotlin/rs09/game/node/entity/player/info/login/LoginParser.kt @@ -13,6 +13,7 @@ import core.game.system.task.Pulse import core.net.amsc.MSPacketRepository import core.net.amsc.ManagementServerState import core.net.amsc.WorldCommunicator +import rs09.auth.AuthResponse import rs09.game.system.SystemLogger import rs09.game.world.GameWorld import rs09.game.world.GameWorld.loginListeners @@ -70,7 +71,7 @@ class LoginParser( } catch (t: Throwable) { t.printStackTrace() try { - flag(Response.ERROR_LOADING_PROFILE) + flag(AuthResponse.ErrorLoadingProfile) Repository.LOGGED_IN_PLAYERS.remove(details.username) } catch (e: Throwable) { e.printStackTrace() @@ -84,10 +85,6 @@ class LoginParser( */ private fun handleLogin() { val p = worldInstance - if (!details.parse()) { - flag(Response.INVALID_LOGIN_SERVER) - return - } val player = p ?: Player(details) player.setAttribute("login_type", type) if (p != null) { // Reconnecting @@ -122,7 +119,7 @@ class LoginParser( Repository.lobbyPlayers.remove(player) Repository.playerNames.remove(player.name) MSPacketRepository.sendPlayerRemoval(player.name) - flag(Response.ERROR_LOADING_PROFILE) + flag(AuthResponse.ErrorLoadingProfile) } //Repository.getPlayerNames().put(player.getName(), player); GameWorld.Pulser.submit(object : Pulse(1) { @@ -140,7 +137,7 @@ class LoginParser( Repository.addPlayer(player) } player.details.session.setObject(player) - flag(Response.SUCCESSFUL) + flag(AuthResponse.Success) player.init() player.monitor.log(player.details.ipAddress, PlayerMonitor.ADDRESS_LOG) player.monitor.log(player.details.serial, PlayerMonitor.ADDRESS_LOG) @@ -156,7 +153,7 @@ class LoginParser( Repository.lobbyPlayers.remove(player) Repository.playerNames.remove(player.name) MSPacketRepository.sendPlayerRemoval(player.name) - flag(Response.ERROR_LOADING_PROFILE) + flag(AuthResponse.ErrorLoadingProfile) } return true } @@ -185,7 +182,7 @@ class LoginParser( Repository.disconnectionQueue.remove(details.username) player.initReconnect() player.isActive = true - flag(Response.SUCCESSFUL) + flag(AuthResponse.Success) player.updateSceneGraph(true) player.configManager.init() LoginConfiguration.configureGameWorld(player) @@ -208,22 +205,22 @@ class LoginParser( //This is supposed to prevent the double-logging issue. Will it work? Who knows. if (Repository.LOGGED_IN_PLAYERS.contains(details.username)) { SystemLogger.logWarn("LOGGED_IN_PLAYERS contains ${details.username}") - return flag(Response.ALREADY_ONLINE) + return flag(AuthResponse.AlreadyOnline) } if (WorldCommunicator.getState() == ManagementServerState.CONNECTING) { - return flag(Response.LOGIN_SERVER_OFFLINE) + return flag(AuthResponse.LoginServerOffline) } if (!details.session.isActive) { return false } if (SystemManager.isUpdating()) { - return flag(Response.UPDATING) + return flag(AuthResponse.Updating) } if (Repository.getPlayerByName(details.username).also { gamePlayer = it } != null && gamePlayer!!.session.isActive) { - return flag(Response.ALREADY_ONLINE) + return flag(AuthResponse.AlreadyOnline) } return if (details.isBanned) { - flag(Response.ACCOUNT_DISABLED) + flag(AuthResponse.AccountDisabled) } else true } @@ -232,9 +229,9 @@ class LoginParser( * @param response the [Response]. * @return `True` if successfully logged in. */ - fun flag(response: Response): Boolean { + fun flag(response: AuthResponse): Boolean { details.session.write(response, true) - return response == Response.SUCCESSFUL + return response == AuthResponse.Success } companion object { diff --git a/Server/src/main/kotlin/rs09/game/system/Auth.kt b/Server/src/main/kotlin/rs09/game/system/Auth.kt new file mode 100644 index 000000000..7bf5e6998 --- /dev/null +++ b/Server/src/main/kotlin/rs09/game/system/Auth.kt @@ -0,0 +1,27 @@ +package rs09.game.system + +import rs09.auth.AuthProvider +import rs09.auth.DevelopmentAuthenticator +import rs09.auth.ProductionAuthenticator +import rs09.storage.AccountStorageProvider +import rs09.storage.InMemoryStorageProvider +import rs09.storage.SQLStorageProvider + +object Auth { + lateinit var authenticator: AuthProvider<*> + lateinit var storageProvider: AccountStorageProvider + + fun configureFor(devMode: Boolean) { + if (devMode) { + authenticator = DevelopmentAuthenticator() + storageProvider = InMemoryStorageProvider() + (authenticator as DevelopmentAuthenticator).configureFor(storageProvider as InMemoryStorageProvider) + SystemLogger.logInfo("[AUTH] Using Development Authenticator with In-Memory Storage") + } else { + authenticator = ProductionAuthenticator() + storageProvider = SQLStorageProvider() + (authenticator as ProductionAuthenticator).configureFor(storageProvider as SQLStorageProvider) + SystemLogger.logInfo("[AUTH] Using Production Authenticator with SQL Storage") + } + } +} \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/game/system/command/sets/ModerationCommandSet.kt b/Server/src/main/kotlin/rs09/game/system/command/sets/ModerationCommandSet.kt index 56111ef6d..fbe793351 100644 --- a/Server/src/main/kotlin/rs09/game/system/command/sets/ModerationCommandSet.kt +++ b/Server/src/main/kotlin/rs09/game/system/command/sets/ModerationCommandSet.kt @@ -2,7 +2,6 @@ package rs09.game.system.command.sets import core.game.node.entity.player.Player import core.game.node.entity.player.info.Rights -import rs09.game.system.command.Command import core.game.system.task.Pulse import rs09.game.world.GameWorld import core.game.world.map.Location @@ -25,7 +24,7 @@ class ModerationCommandSet : CommandSet(Privilege.MODERATOR){ * Kick a player * ============================================================================================================= */ - define("kick", Privilege.ADMIN){ player, args -> + define("kick", Privilege.MODERATOR){ player, args -> val playerToKick: Player? = Repository.getPlayerByName(args[1]) if (playerToKick != null) { playerToKick.clear(true) @@ -38,6 +37,100 @@ class ModerationCommandSet : CommandSet(Privilege.MODERATOR){ * ============================================================================================================= */ + /** + * Ban a player + * ============================================================================================================= + */ + define("ban", Privilege.ADMIN){ player, args -> + val name = args[1] + if(!GameWorld.accountStorage.checkUsernameTaken(name)) { + reject(player, "Invalid username: $name") + } + val playerToKick: Player? = Repository.getPlayerByName(name) + val durationString = args[2] + val durationTokens = durationString.toCharArray() + var intToken = "" + var durationMillis = 0L + var durationUnit: TimeUnit = TimeUnit.NANOSECONDS + for(token in durationTokens){ + if(token.toString().toIntOrNull() != null) intToken += token + else { + val durationInt: Int = (intToken.toIntOrNull() ?: -1).also { if(it == -1) reject(player, "Invalid duration: $intToken") } + durationUnit = when(token) { + 'd' -> TimeUnit.DAYS + 's' -> TimeUnit.SECONDS + 'm' -> TimeUnit.MINUTES + 'h' -> TimeUnit.HOURS + else -> TimeUnit.SECONDS + } + durationMillis = durationUnit.toMillis(durationInt.toLong()) + } + } + + playerToKick?.details?.accountInfo?.banEndTime = System.currentTimeMillis() + durationMillis + playerToKick?.clear(true) + GameWorld.Pulser.submit(object : Pulse(2) { + override fun pulse(): Boolean { + val info = GameWorld.accountStorage.getAccountInfo(name) + info.banEndTime = System.currentTimeMillis() + durationMillis + GameWorld.accountStorage.update(info) + return true + } + }) + + notify(player, "Banned user $name for $intToken ${durationUnit.name.toLowerCase()}.") + } + /** + * ============================================================================================================= + */ + + /** + * Mute a player + * ============================================================================================================= + */ + define("mute", Privilege.MODERATOR){ player, args -> + val name = args[1] + if(!GameWorld.accountStorage.checkUsernameTaken(name)) { + reject(player, "Invalid username: $name") + } + val playerToMute: Player? = Repository.getPlayerByName(name) + val durationString = args[2] + val durationTokens = durationString.toCharArray() + var intToken = "" + var durationMillis = 0L + var durationUnit: TimeUnit = TimeUnit.NANOSECONDS + for(token in durationTokens){ + if(token.toString().toIntOrNull() != null) intToken += token + else { + val durationInt: Int = (intToken.toIntOrNull() ?: -1).also { if(it == -1) reject(player, "Invalid duration: $intToken") } + durationUnit = when(token) { + 'd' -> TimeUnit.DAYS + 's' -> TimeUnit.SECONDS + 'm' -> TimeUnit.MINUTES + 'h' -> TimeUnit.HOURS + else -> TimeUnit.SECONDS + } + durationMillis = durationUnit.toMillis(durationInt.toLong()) + } + } + + playerToMute?.details?.accountInfo?.muteEndTime = System.currentTimeMillis() + durationMillis + if(playerToMute == null) { //Player was offline at the time + GameWorld.Pulser.submit(object : Pulse(2) { + override fun pulse(): Boolean { + val info = GameWorld.accountStorage.getAccountInfo(name) + info.muteEndTime = System.currentTimeMillis() + durationMillis + GameWorld.accountStorage.update(info) + return true + } + }) + } + + notify(player, "Muted user $name for $intToken ${durationUnit.name.toLowerCase()}.") + } + /** + * ============================================================================================================= + */ /** * Jail a player diff --git a/Server/src/main/kotlin/rs09/game/system/command/sets/SystemCommandSet.kt b/Server/src/main/kotlin/rs09/game/system/command/sets/SystemCommandSet.kt index 9051ea006..b062537aa 100644 --- a/Server/src/main/kotlin/rs09/game/system/command/sets/SystemCommandSet.kt +++ b/Server/src/main/kotlin/rs09/game/system/command/sets/SystemCommandSet.kt @@ -1,17 +1,18 @@ package rs09.game.system.command.sets -import core.cache.Cache -import core.cache.def.Definition +import api.InputType +import api.runTask +import api.sendDialogue +import api.sendInputDialogue import core.cache.def.impl.ItemDefinition -import core.game.node.entity.player.info.login.Response -import core.game.node.entity.player.info.portal.PlayerSQLManager import core.game.node.item.Item import core.game.system.SystemManager import core.game.system.SystemState import core.plugin.Initializable import org.rs09.consts.Items -import rs09.game.system.command.Command +import rs09.game.system.SystemLogger import rs09.game.system.command.Privilege +import rs09.game.world.GameWorld import rs09.game.world.repository.Repository import kotlin.system.exitProcess @@ -40,31 +41,29 @@ class SystemCommandSet : CommandSet(Privilege.ADMIN) { * Allows a player to reset their password */ define("resetpassword", Privilege.STANDARD) { player, args -> - if (args.size != 3) { - reject(player, "Usage: ::resetpassword current new", "WARNING: THIS IS PERMANENT.", "WARNING: PASSWORD CAN NOT CONTAIN SPACES.") + sendInputDialogue(player, InputType.STRING_SHORT, "Enter Current Password:"){value -> + val pass = value.toString() + SystemLogger.logInfo(pass) + runTask(player) { + if (GameWorld.authenticator.checkPassword(player, pass)) { + sendInputDialogue(player, InputType.STRING_SHORT, "Enter New Password:") { value2 -> + val newPass = value2.toString() + if (pass == newPass) { + sendDialogue(player, "Failed: Passwords Match") + } else if (newPass.length !in 5..20) { + sendDialogue(player, "Failed: Password Too Long Or Too Short") + } else if (newPass == player.details.accountInfo.username) { + sendDialogue(player, "Failed: Password Is Username") + } else { + GameWorld.authenticator.updatePassword(player.details.accountInfo.username, newPass) + sendDialogue(player, "Success: Password Updated!") + } + } + } else { + sendDialogue(player, "Fail: Wrong Password.") + } + } } - val oldPass = args[1] - var newPass = args[2] - - if (PlayerSQLManager.getCredentialResponse(player.details.username, oldPass) != Response.SUCCESSFUL) { - reject(player, "INVALID PASSWORD!") - } - - if (newPass.length < 5 || newPass.length > 20) { - reject(player, "NEW PASSWORD MUST BE BETWEEN 5 AND 20 CHARACTERS") - } - - if (newPass == player.username) { - reject(player, "PASSWORD CAN NOT BE SAME AS USERNAME.") - } - - if (newPass == oldPass) { - reject(player, "PASSWORDS CAN NOT BE THE SAME") - } - - newPass = SystemManager.getEncryption().hashPassword(newPass) - PlayerSQLManager.updatePassword(player.username.toLowerCase().replace(" ", "_"), newPass) - notify(player, "Password updated successfully.") } /** @@ -75,9 +74,9 @@ class SystemCommandSet : CommandSet(Privilege.ADMIN) { reject(player, "Usage: ::resetpasswordother user new", "WARNING: THIS IS PERMANENT.", "WARNING: PASSWORD CAN NOT CONTAIN SPACES.") } val otherUser = args[1] - var newPass = args[2] + val newPass = args[2] - if (PlayerSQLManager.hasSqlAccount(otherUser, "username")) { + if (GameWorld.accountStorage.checkUsernameTaken(otherUser)) { if (newPass.length < 5 || newPass.length > 20) { reject(player, "NEW PASSWORD MUST BE BETWEEN 5 AND 20 CHARACTERS") @@ -87,8 +86,7 @@ class SystemCommandSet : CommandSet(Privilege.ADMIN) { reject(player, "PASSWORD CAN NOT BE SAME AS USERNAME.") } - newPass = SystemManager.getEncryption().hashPassword(newPass) - PlayerSQLManager.updatePassword(otherUser.toLowerCase().replace(" ","_"),newPass) + GameWorld.authenticator.updatePassword(otherUser, newPass) notify(player, "Password updated successfully.") } else { diff --git a/Server/src/main/kotlin/rs09/game/world/GameWorld.kt b/Server/src/main/kotlin/rs09/game/world/GameWorld.kt index aeefd8d5c..81a5bb729 100644 --- a/Server/src/main/kotlin/rs09/game/world/GameWorld.kt +++ b/Server/src/main/kotlin/rs09/game/world/GameWorld.kt @@ -12,13 +12,15 @@ import core.game.world.map.Location import core.game.world.map.RegionManager import core.plugin.CorePluginTypes.StartupPlugin import core.tools.RandomFunction -import core.tools.mysql.DatabaseManager import rs09.ServerConstants +import rs09.auth.AuthProvider +import rs09.game.system.Auth import rs09.game.system.SystemLogger import rs09.game.system.SystemLogger.logInfo import rs09.game.system.config.ConfigParser import rs09.game.world.repository.Repository import rs09.plugin.ClassScanner +import rs09.storage.AccountStorageProvider import rs09.worker.MajorUpdateWorker import java.text.SimpleDateFormat import java.util.* @@ -83,14 +85,17 @@ object GameWorld { */ @JvmStatic var settings: GameSettings? = null + @JvmStatic + val authenticator: AuthProvider<*> + get() = Auth.authenticator + @JvmStatic + val accountStorage: AccountStorageProvider + get() = Auth.storageProvider /** * The current amount of (600ms) cycles elapsed. */ @JvmStatic var ticks = 0 - @JvmStatic - var databaseManager: DatabaseManager? = null - private set @JvmStatic var Pulser = PulseRunner() @@ -158,9 +163,12 @@ object GameWorld { fun prompt(run: Boolean, directory: String?){ logInfo("Prompting ${settings?.name} Game World...") Cache.init(ServerConstants.CACHE_PATH) - databaseManager = DatabaseManager(ServerConstants.DATABASE) - databaseManager!!.connect() - configParser.prePlugin() + //go overboard with checks to make sure dev mode authenticator never triggers on live + Auth.configureFor( + settings!!.isDevMode + && ServerConstants.MS_SECRET_KEY == "2009scape_development" + ) + ConfigParser().prePlugin() ClassScanner.scanClasspath() ClassScanner.loadPureInterfaces() worldPersists.forEach { it.parse() } diff --git a/Server/src/main/kotlin/rs09/game/world/repository/DisconnectionQueue.kt b/Server/src/main/kotlin/rs09/game/world/repository/DisconnectionQueue.kt index 5ba79b7b2..1081bd886 100644 --- a/Server/src/main/kotlin/rs09/game/world/repository/DisconnectionQueue.kt +++ b/Server/src/main/kotlin/rs09/game/world/repository/DisconnectionQueue.kt @@ -195,12 +195,6 @@ class DisconnectionQueue { fun save(player: Player, sql: Boolean): Boolean { try { PlayerParser.save(player) - if (sql) { - player.details.sqlManager.update(player) - player.details.save() - /*SQLEntryHandler.write(HighscoreSQLHandler(player)) - SQLEntryHandler.write(PlayerLogSQLHandler(player.monitor, player.name))*/ - } return true } catch (t: Throwable) { t.printStackTrace() diff --git a/Server/src/main/kotlin/rs09/net/event/LoginReadEvent.kt b/Server/src/main/kotlin/rs09/net/event/LoginReadEvent.kt index c4addd162..fca310685 100644 --- a/Server/src/main/kotlin/rs09/net/event/LoginReadEvent.kt +++ b/Server/src/main/kotlin/rs09/net/event/LoginReadEvent.kt @@ -1,190 +1,39 @@ package rs09.net.event -import core.cache.Cache -import core.cache.crypto.ISAACCipher -import core.cache.crypto.ISAACPair -import core.cache.misc.buffer.ByteBufferUtils import core.game.node.entity.player.info.ClientInfo import core.game.node.entity.player.info.PlayerDetails -import core.game.node.entity.player.info.UIDInfo -import core.game.node.entity.player.info.login.LoginType -import core.game.node.entity.player.info.login.Response -import core.game.node.entity.player.info.portal.PlayerSQLManager -import core.game.system.task.TaskExecutor -import core.net.Constants import core.net.IoReadEvent import core.net.IoSession -import core.net.amsc.WorldCommunicator -import core.tools.StringUtils -import rs09.ServerConstants -import rs09.game.node.entity.player.info.login.LoginParser -import rs09.game.system.SystemLogger -import rs09.game.world.repository.Repository -import java.lang.Runnable -import java.math.BigInteger +import rs09.auth.AuthResponse +import rs09.game.world.GameWorld +import rs09.net.packet.`in`.Login import java.nio.ByteBuffer -import java.util.* /** * Handles login reading events. - * @author Emperor + * @author Ceikry */ -class LoginReadEvent -/** - * Constructs a new `LoginReadEvent`. - * @param session The session. - * @param buffer The buffer with data to read from. - */ -(session: IoSession?, buffer: ByteBuffer?) : IoReadEvent(session, buffer) { +class LoginReadEvent(session: IoSession?, buffer: ByteBuffer?) : IoReadEvent(session, buffer) { override fun read(session: IoSession, buffer: ByteBuffer) { - SystemLogger.logInfo("login read") - val opcode: Int = buffer.get().toInt() - if (buffer.short.toInt() != buffer.remaining()) { - session.write(Response.BAD_SESSION_ID) + val (response, info) = Login.decodeFromBuffer(buffer) + + if(response != AuthResponse.Success || info == null) { + session.write(response) return } - val build = buffer.int - if (build != Constants.REVISION) { // || buffer.getInt() != Constants.CLIENT_BUILD) { - session.write(Response.UPDATED) + + val (authResponse, accountInfo) = GameWorld.authenticator.checkLogin(info.username, info.password) + + if(authResponse != AuthResponse.Success || accountInfo == null) { + session.write(authResponse) return } - when (opcode) { - 12 -> println("User details event detected") - 16, 18 -> decodeWorld(opcode, session, buffer) - else -> { - SystemLogger.logErr("[Login] Unhandled login type [opcode=$opcode]!") - session.disconnect() - } - } - } - companion object { + val details = PlayerDetails(info.username) + details.accountInfo = accountInfo + session.clientInfo = ClientInfo(info.displayMode, info.windowMode, info.screenWidth, info.screenHeight) + session.isaacPair = info.isaacPair - /** - * Decodes a world login request. - * @param session The session. - * @param buffer The buffer to read from. - */ - private fun decodeWorld(opcode: Int, session: IoSession, buffer: ByteBuffer) { - var buffer = buffer - val d = buffer.get() // Memory? - val e = buffer.get() // no advertisement = 1 - val f = buffer.get() // 1 - val windowMode = buffer.get().toInt() // Screen size mode - val screenWidth = buffer.short.toInt() // Screen size Width - val screenHeight = buffer.short.toInt() // Screen size Height - val displayMode = buffer.get().toInt() // Display mode - val data = ByteArray(24) // random.dat data. - buffer[data] - ByteBufferUtils.getString(buffer) - buffer.int // Affiliate id - buffer.int // Hash containing a bunch of settings - val curpackets = buffer.short //Current interface packet counter. - for (i in Cache.getIndexes().indices) { - val crc = if (Cache.getIndexes()[i] == null) 0 else Cache.getIndexes()[i].information.informationContainer.crc - if (crc != buffer.int && crc != 0) { - /*session.write(Response.UPDATED); - return;*/ - } - } - buffer = getRSABlock(buffer) - buffer.rewind() - if(buffer.get().toInt() != 10){ - session.write(Response.COULD_NOT_LOGIN) - return - } - val isaacSeed = getISAACSeed(buffer) - val inCipher = ISAACCipher(isaacSeed) - for(i in 0..curpackets){ - inCipher.nextValue - } - for (i in 0..3) { - isaacSeed[i] += 50 - } - val outCipher = ISAACCipher(isaacSeed) - session.isaacPair = ISAACPair(inCipher, outCipher) - session.clientInfo = ClientInfo(displayMode, windowMode, screenWidth, screenHeight) - val b = buffer - SystemLogger.logInfo("spawning thread to handle login") - TaskExecutor.executeSQL { - SystemLogger.logInfo("login thread start") - Thread.currentThread().name = "Login Password Response" - SystemLogger.logInfo("login thread named") - try { - val username = StringUtils.longToString(b.long) - SystemLogger.logInfo("got username") - val password = ByteBufferUtils.getString(b) - SystemLogger.logInfo("got password") - val response = PlayerSQLManager.getCredentialResponse(username, password) - SystemLogger.logInfo("got sql response") - if (response != Response.SUCCESSFUL) { - SystemLogger.logInfo("not success :(") - session.write(response, true) - return@executeSQL - } - SystemLogger.logInfo("great success, attempting login") - login(PlayerDetails(username, password), session, b, opcode) - SystemLogger.logInfo("done") - } catch (e: Exception) { - SystemLogger.logInfo("big whoops") - e.printStackTrace() - session.write(Response.COULD_NOT_LOGIN) - } - SystemLogger.logInfo("end login thread") - } - } - - /** - * Handles the login procedure after we check an acc is registered & certified. - * @param details the player's details. - * @param session the session. - * @param buffer the byte buffer. - * @param opcode the opcode. - */ - @JvmStatic - private fun login(details: PlayerDetails, session: IoSession, buffer: ByteBuffer, opcode: Int) { - SystemLogger.logInfo("login") - if(!Repository.LOGGED_IN_PLAYERS.contains(details.username)) - Repository.LOGGED_IN_PLAYERS.add(details.username) - val parser = LoginParser(details, LoginType.fromType(opcode)) - details.session = session - details.info.translate(UIDInfo(details.ipAddress, ByteBufferUtils.getString(buffer), ByteBufferUtils.getString(buffer), ByteBufferUtils.getString(buffer))) - if (WorldCommunicator.isEnabled()) { - WorldCommunicator.register(parser) - } else { - TaskExecutor.executeSQL {parser.run()} - } - } - - /** - * Gets the ISAAC seed from the buffer. - * @param buffer The buffer to read from. - * @return The ISAAC seed. - */ - @JvmStatic - fun getISAACSeed(buffer: ByteBuffer): IntArray { - val seed = IntArray(4) - for (i in 0..3) { - seed[i] = buffer.int - } - return seed - } - - /** - * Gets the RSA block buffer. - * @param buffer The buffer to get the RSA block from. - * @return The RSA block buffer. - */ - @JvmStatic - fun getRSABlock(buffer: ByteBuffer): ByteBuffer { - SystemLogger.logInfo("getRSABlock") - fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) } - SystemLogger.logInfo(buffer.array().sliceArray(0..1500).toHex()) - val numBytes = 256 + buffer.get() - val encryptedByteArray = ByteArray(numBytes) - buffer.get(encryptedByteArray) - val encryptedBytes = BigInteger(encryptedByteArray) - return ByteBuffer.wrap(encryptedBytes.modPow(ServerConstants.EXPONENT, ServerConstants.MODULUS).toByteArray()) - } + Login.proceedWith(session, details, info.opcode) } } \ No newline at end of file diff --git a/Server/src/main/kotlin/rs09/net/packet/in/Login.kt b/Server/src/main/kotlin/rs09/net/packet/in/Login.kt new file mode 100644 index 000000000..8e7cb9a81 --- /dev/null +++ b/Server/src/main/kotlin/rs09/net/packet/in/Login.kt @@ -0,0 +1,114 @@ +package rs09.net.packet.`in` + +import core.cache.crypto.ISAACCipher +import core.cache.crypto.ISAACPair +import core.cache.misc.buffer.ByteBufferUtils +import core.game.node.entity.player.info.PlayerDetails +import core.game.node.entity.player.info.UIDInfo +import core.game.node.entity.player.info.login.LoginType +import core.net.Constants +import core.net.IoSession +import core.net.amsc.WorldCommunicator +import core.tools.StringUtils +import rs09.ServerConstants +import rs09.auth.AuthResponse +import rs09.game.node.entity.player.info.login.LoginParser +import rs09.game.system.SystemLogger +import rs09.game.world.repository.Repository +import java.math.BigInteger +import java.nio.BufferUnderflowException +import java.nio.ByteBuffer + +object Login { + private const val ENCRYPTION_VERIFICATION_BYTE: Int = 10 + private const val NORMAL_LOGIN_OP = 16 + private const val RECONNECT_LOGIN_OP = 18 + const val CACHE_INDEX_COUNT = 29 + + fun decodeFromBuffer(buffer: ByteBuffer) : Pair { + try { + val info = LoginInfo.createDefault() + + info.opcode = buffer.get().toInt() + if (buffer.short.toInt() != buffer.remaining()) { + return Pair(AuthResponse.BadSessionID, null) + } + val revision = buffer.int + if (revision != Constants.REVISION) { + return Pair(AuthResponse.Updated, null) + } + if (info.opcode != NORMAL_LOGIN_OP && info.opcode != RECONNECT_LOGIN_OP) { + SystemLogger.logInfo("Invalid Login Opcode: ${info.opcode}") + return Pair(AuthResponse.InvalidLoginServer, null) + } + + noop(buffer) + info.showAds = buffer.get().toInt() == 1 + noop(buffer) + info.windowMode = buffer.get().toInt() + info.screenWidth = buffer.short.toInt() + info.screenHeight = buffer.short.toInt() + info.displayMode = buffer.get().toInt() + noop(buffer, 24) //Skip past a bunch of random (actually random) data the client sends + ByteBufferUtils.getString(buffer) //same as above + info.adAffiliateId = buffer.int + info.settingsHash = buffer.int + info.currentPacketCount = buffer.short.toInt() + + //Read client-reported CRC sums + for (i in 0 until CACHE_INDEX_COUNT) info.crcSums[i] = buffer.int + + val decryptedBuffer = decryptRSABuffer(buffer, ServerConstants.EXPONENT, ServerConstants.MODULUS) + decryptedBuffer.rewind() + + if (decryptedBuffer.get().toInt() != ENCRYPTION_VERIFICATION_BYTE) { + return Pair(AuthResponse.UnexpectedError, info) + } + + info.isaacPair = produceISAACPairFrom(decryptedBuffer) + info.username = StringUtils.longToString(decryptedBuffer.long) + info.password = ByteBufferUtils.getString(decryptedBuffer) + return Pair(AuthResponse.Success, info) + } catch (e: Exception) { + SystemLogger.logErr("Exception encountered during login packet parsing! See stack trace below.") + e.printStackTrace() + return Pair(AuthResponse.UnexpectedError, null) + } + } + + private fun produceISAACPairFrom(buffer: ByteBuffer): ISAACPair { + val incomingSeed = IntArray(4) + for(i in incomingSeed.indices) { + incomingSeed[i] = buffer.int + } + val inCipher = ISAACCipher(incomingSeed) + for(i in incomingSeed.indices) { + incomingSeed[i] += 50 + } + val outCipher = ISAACCipher(incomingSeed) + return ISAACPair(inCipher, outCipher) + } + + @JvmStatic fun decryptRSABuffer(buffer: ByteBuffer, exponent: BigInteger, modulus: BigInteger): ByteBuffer { + return try { + val numBytes = buffer.get().toInt() and 0xFF + val encryptedBytes = ByteArray(numBytes) + buffer[encryptedBytes] + + val encryptedBigInt = BigInteger(encryptedBytes) + ByteBuffer.wrap(encryptedBigInt.modPow(exponent, modulus).toByteArray()) + } catch (e: BufferUnderflowException) { + ByteBuffer.wrap(byteArrayOf(-1)) + } + } + + private fun noop(buffer: ByteBuffer, amount: Int = 1) {buffer[ByteArray(amount)]} + + fun proceedWith(session: IoSession, details: PlayerDetails, opcode: Int) { + if (!Repository.LOGGED_IN_PLAYERS.contains(details.username)) + Repository.LOGGED_IN_PLAYERS.add(details.username) + details.session = session + details.info.translate(UIDInfo(details.ipAddress, "DEPRECATED", "DEPRECATED", "DEPRECATED")) + WorldCommunicator.register(LoginParser(details, LoginType.fromType(opcode))) + } +} diff --git a/Server/src/main/kotlin/rs09/net/packet/in/LoginInfo.kt b/Server/src/main/kotlin/rs09/net/packet/in/LoginInfo.kt new file mode 100644 index 000000000..f0da764c5 --- /dev/null +++ b/Server/src/main/kotlin/rs09/net/packet/in/LoginInfo.kt @@ -0,0 +1,30 @@ +package rs09.net.packet.`in` + +import core.cache.crypto.ISAACCipher +import core.cache.crypto.ISAACPair + +class LoginInfo( + var showAds: Boolean, //Unused + var windowMode : Int, + var screenWidth: Int, + var screenHeight: Int, + var displayMode: Int, + var adAffiliateId: Int, //Unused + var settingsHash: Int, + var currentPacketCount: Int, + var username: String, + var password: String, + var isaacPair: ISAACPair, + var opcode: Int, + var crcSums: IntArray +) { + companion object { + fun createDefault(): LoginInfo { + return LoginInfo(false, 0, 0, 0, 0, 0, 0, 0, "", "", ISAACPair(ISAACCipher(intArrayOf()), ISAACCipher(intArrayOf())), 0, IntArray(Login.CACHE_INDEX_COUNT)) + } + } + + override fun toString(): String { + return "ads:$showAds,wm:$windowMode,sw:$screenWidth,sh:$screenHeight,dm:$displayMode,adid:$adAffiliateId,settings:$settingsHash,pkt:$currentPacketCount,un:$username,pw:$password" + } +} diff --git a/Server/src/main/kotlin/rs09/storage/AccountStorageProvider.kt b/Server/src/main/kotlin/rs09/storage/AccountStorageProvider.kt new file mode 100644 index 000000000..6151ba72b --- /dev/null +++ b/Server/src/main/kotlin/rs09/storage/AccountStorageProvider.kt @@ -0,0 +1,11 @@ +package rs09.storage + +import rs09.auth.UserAccountInfo + +interface AccountStorageProvider { + fun checkUsernameTaken(username: String): Boolean + fun getAccountInfo(username: String): UserAccountInfo + fun store(info: UserAccountInfo) + fun update(info: UserAccountInfo) + fun remove(info: UserAccountInfo) +} diff --git a/Server/src/main/kotlin/rs09/storage/InMemoryStorageProvider.kt b/Server/src/main/kotlin/rs09/storage/InMemoryStorageProvider.kt new file mode 100644 index 000000000..54a09fff9 --- /dev/null +++ b/Server/src/main/kotlin/rs09/storage/InMemoryStorageProvider.kt @@ -0,0 +1,27 @@ +package rs09.storage + +import rs09.auth.UserAccountInfo + +class InMemoryStorageProvider : AccountStorageProvider { + private val storage = HashMap() + + override fun checkUsernameTaken(username: String): Boolean { + return storage[username] != null + } + + override fun getAccountInfo(username: String): UserAccountInfo { + return storage[username] ?: UserAccountInfo.createDefault() + } + + override fun store(info: UserAccountInfo) { + storage[info.username] = info + } + + override fun update(info: UserAccountInfo) { + storage[info.username] = info + } + + override fun remove(info: UserAccountInfo) { + storage.remove(info.username) + } +} diff --git a/Server/src/main/kotlin/rs09/storage/SQLStorageProvider.kt b/Server/src/main/kotlin/rs09/storage/SQLStorageProvider.kt new file mode 100644 index 000000000..d27a96b6c --- /dev/null +++ b/Server/src/main/kotlin/rs09/storage/SQLStorageProvider.kt @@ -0,0 +1,214 @@ +package rs09.storage + +import rs09.auth.UserAccountInfo +import java.lang.Long.max +import java.sql.* + +class SQLStorageProvider : AccountStorageProvider { + var connectionString = "" + var connectionUsername = "" + var connectionPassword = "" + + fun getConnection(): Connection { + Class.forName("com.mysql.cj.jdbc.Driver") + return DriverManager.getConnection(connectionString, connectionUsername, connectionPassword) + } + + fun configure(host: String, databaseName: String, username: String, password: String) { + connectionString = "jdbc:mysql://$host/$databaseName?useTimezone=true&serverTimezone=UTC" + connectionUsername = username + connectionPassword = password + } + + override fun checkUsernameTaken(username: String): Boolean { + val conn = getConnection() + conn.use { + val compiledUsernameQuery = it.prepareStatement(usernameQuery) + compiledUsernameQuery.setString(1, username.toLowerCase()) + val result = compiledUsernameQuery.executeQuery() + val exists = result.next() + result.close() + return exists + } + } + + override fun getAccountInfo(username: String): UserAccountInfo { + val conn = getConnection() + conn.use { con -> + val compiledAccountInfoQuery = con.prepareStatement(accountInfoQuery) + compiledAccountInfoQuery.setString(1, username.toLowerCase()) + val result = compiledAccountInfoQuery.executeQuery() + if (result.next()) { + val userData = UserAccountInfo.createDefault() + userData.username = username + + result.getString(2) ?.let { userData.password = it } + result.getInt(3) .let { userData.uid = it } + result.getInt(4) .let { userData.rights = it } + result.getInt(5) .let { userData.credits = it } + result.getString(6) ?.let { userData.ip = it } + result.getString(7) ?.let { userData.lastUsedIp = it } + result.getLong(8) .let { userData.muteEndTime = max(0L, it) } + result.getLong(9) .let { userData.banEndTime = max(0L, it) } + result.getString(10)?.let { userData.contacts = it } + result.getString(11)?.let { userData.blocked = it } + result.getString(12)?.let { userData.clanName = it } + result.getString(13)?.let { userData.currentClan = it } + result.getString(14)?.let { userData.clanReqs = it } + result.getLong(15) .let { userData.timePlayed = max(0L, it) } + result.getLong(16) .let { userData.lastLogin = max(0L, it) } + result.getBoolean(17).let { userData.online = it } + + return userData + } else { + return UserAccountInfo.createDefault() + } + } + } + + override fun store(info: UserAccountInfo) { + val conn = getConnection() + conn.use { + val compiledInsertInfoQuery = it.prepareStatement(insertInfoQuery, Statement.RETURN_GENERATED_KEYS) + val emptyInfo = UserAccountInfo.createDefault() + if (info == emptyInfo) { + throw IllegalStateException("Tried to store empty data!") + } + emptyInfo.username = info.username + if (info == emptyInfo) { + throw IllegalStateException("Tried to store empty data!") + } + + if (checkUsernameTaken(info.username)) { + throw SQLDataException("Account already exists!") + } + compiledInsertInfoQuery.setString(1, info.username) + compiledInsertInfoQuery.setString(2, info.password) + compiledInsertInfoQuery.setInt(3, info.rights) + compiledInsertInfoQuery.setInt(4, info.credits) + compiledInsertInfoQuery.setString(5, info.ip) + compiledInsertInfoQuery.setString(6, info.ip) + compiledInsertInfoQuery.setLong(7, info.muteEndTime) + compiledInsertInfoQuery.setLong(8, info.banEndTime) + compiledInsertInfoQuery.setString(9, info.contacts) + compiledInsertInfoQuery.setString(10, info.blocked) + compiledInsertInfoQuery.setString(11, info.clanName) + compiledInsertInfoQuery.setString(12, info.currentClan) + compiledInsertInfoQuery.setString(13, info.clanReqs) + compiledInsertInfoQuery.setLong(14, info.timePlayed) + compiledInsertInfoQuery.setLong(15, info.lastLogin) + compiledInsertInfoQuery.setBoolean(16, info.online) + compiledInsertInfoQuery.execute() + val result = compiledInsertInfoQuery.generatedKeys + if (result.next()) { + info.uid = result.getInt(1) + } + } + } + + override fun update(info: UserAccountInfo) { + val conn = getConnection() + conn.use { + val compiledUpdateInfoQuery = it.prepareStatement(updateInfoQuery) + val emptyInfo = UserAccountInfo.createDefault() + if (info == emptyInfo) { + throw IllegalStateException("Tried to store empty data!") + } + emptyInfo.username = info.username + if (info == emptyInfo) { + throw IllegalStateException("Tried to store empty data!") + } + emptyInfo.password = info.password + if (info == emptyInfo) { + throw IllegalStateException("Tried to store empty data!") + } + + compiledUpdateInfoQuery.setString(1, info.username) + compiledUpdateInfoQuery.setString(2, info.password) + compiledUpdateInfoQuery.setInt(3, info.rights) + compiledUpdateInfoQuery.setInt(4, info.credits) + compiledUpdateInfoQuery.setString(5, info.ip) + compiledUpdateInfoQuery.setLong(6, info.muteEndTime) + compiledUpdateInfoQuery.setLong(7, info.banEndTime) + compiledUpdateInfoQuery.setString(8, info.contacts) + compiledUpdateInfoQuery.setString(9, info.blocked) + compiledUpdateInfoQuery.setString(10, info.clanName) + compiledUpdateInfoQuery.setString(11, info.currentClan) + compiledUpdateInfoQuery.setString(12, info.clanReqs) + compiledUpdateInfoQuery.setLong(13, info.timePlayed) + compiledUpdateInfoQuery.setLong(14, info.lastLogin) + compiledUpdateInfoQuery.setBoolean(15, info.online) + compiledUpdateInfoQuery.setInt(16, info.uid) + compiledUpdateInfoQuery.execute() + } + } + + override fun remove(info: UserAccountInfo) { + val conn = getConnection() + conn.use { + val compiledRemoveInfoQuery = it.prepareStatement(removeInfoQuery) + compiledRemoveInfoQuery.setString(1, info.username) + compiledRemoveInfoQuery.execute() + } + } + + companion object { + private const val usernameQuery = "SELECT username FROM members WHERE username = ?;" + private const val removeInfoQuery = "DELETE FROM members WHERE username = ?;" + private const val accountInfoQuery = "SELECT " + + "username," + + "password," + + "UID," + + "rights," + + "credits," + + "ip," + + "lastGameIp," + + "muteTime," + + "banTime," + + "contacts," + + "blocked," + + "clanName," + + "currentClan," + + "clanReqs," + + "timePlayed," + + "lastLogin," + + "online" + + " FROM members WHERE username = ?;" + private const val insertInfoQuery = "INSERT INTO members (" + + "username," + + "password," + + "rights," + + "credits," + + "ip," + + "lastGameIp," + + "muteTime," + + "banTime," + + "contacts," + + "blocked," + + "clanName," + + "currentClan," + + "clanReqs," + + "timePlayed," + + "lastLogin," + + "online" + + ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);" + + private const val updateInfoQuery = "UPDATE members SET " + + "username = ?," + + "password = ?," + + "rights = ?," + + "credits = ?," + + "lastGameIp = ?," + + "muteTime = ?," + + "banTime = ?," + + "contacts = ?," + + "blocked = ?," + + "clanName = ?," + + "currentClan = ?," + + "clanReqs = ?," + + "timePlayed = ?," + + "lastLogin = ?," + + "online = ?" + + " WHERE uid = ?;" + } +} diff --git a/Server/src/test/kotlin/TestUtils.kt b/Server/src/test/kotlin/TestUtils.kt index 002d115c0..a949f9364 100644 --- a/Server/src/test/kotlin/TestUtils.kt +++ b/Server/src/test/kotlin/TestUtils.kt @@ -19,7 +19,7 @@ import java.nio.ByteBuffer object TestUtils { fun getMockPlayer(name: String, ironman: IronmanMode = IronmanMode.NONE): Player { - val p = Player(PlayerDetails(name, name)) + val p = Player(PlayerDetails(name)) p.details.session = MockSession() p.ironmanManager.mode = ironman Repository.addPlayer(p) diff --git a/Server/src/test/kotlin/core/auth/DevelopmentAuthenticatorTests.kt b/Server/src/test/kotlin/core/auth/DevelopmentAuthenticatorTests.kt new file mode 100644 index 000000000..f3b9ba5ba --- /dev/null +++ b/Server/src/test/kotlin/core/auth/DevelopmentAuthenticatorTests.kt @@ -0,0 +1,55 @@ +package core.auth + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import rs09.auth.DevelopmentAuthenticator +import rs09.auth.AuthResponse +import rs09.auth.UserAccountInfo +import rs09.storage.InMemoryStorageProvider + +class DevelopmentAuthenticatorTests { + private val authProvider = DevelopmentAuthenticator() + private val storageProvider = InMemoryStorageProvider() + + init { + authProvider.configureFor(storageProvider) + } + + @Test fun shouldAllowCheckingIfAccountExists() { + val info = UserAccountInfo.createDefault() + info.username = "Billy" + Assertions.assertEquals(true, authProvider.canCreateAccountWith(info)) + authProvider.createAccountWith(info) + Assertions.assertEquals(false, authProvider.canCreateAccountWith(info)) + } + + @Test fun loginWithValidAccountInfoReturnsSuccess() { + val info = UserAccountInfo.createDefault() + info.username = "Billy" + authProvider.createAccountWith(info) + Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("Billy", "").first) + } + + //Development authenticator should work regardless if account exists or not. + @Test fun loginWithInvalidAccountInfoReturnsSuccess() { + val info = UserAccountInfo.createDefault() + info.username = "Billy" + authProvider.createAccountWith(info) + Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("Bilbo", "ebbeb").first) + } + + @Test fun loginUsernameIsNotCaseSensitive() { + val info = UserAccountInfo.createDefault() + info.username = "Billy" + authProvider.createAccountWith(info) + Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("Billy", "").first) + Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("billy", "").first) + } + + //Development authenticator should basically bypass needing/creating an account entirely. useful for SP too. + @Test fun loginToUnregisteredAccountCreatesIt() { + authProvider.checkLogin("masterbaggins", "whatever") + val info = storageProvider.getAccountInfo("masterbaggins") + Assertions.assertEquals(2, info.rights) + } +} \ No newline at end of file diff --git a/Server/src/test/kotlin/core/auth/ProductionAuthenticatorTests.kt b/Server/src/test/kotlin/core/auth/ProductionAuthenticatorTests.kt new file mode 100644 index 000000000..c06ef15a7 --- /dev/null +++ b/Server/src/test/kotlin/core/auth/ProductionAuthenticatorTests.kt @@ -0,0 +1,62 @@ +package core.auth + +import org.junit.Assert +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import rs09.auth.AuthResponse +import rs09.auth.ProductionAuthenticator +import rs09.auth.UserAccountInfo +import rs09.storage.InMemoryStorageProvider + +class ProductionAuthenticatorTests { + companion object { + private val authProvider = ProductionAuthenticator() + private val storageProvider = InMemoryStorageProvider() + + init { + authProvider.configureFor(storageProvider) + } + + @BeforeAll @JvmStatic fun createTestAccount() { + val details = UserAccountInfo.createDefault() + details.username = "test" + details.password = "testing" + if(!storageProvider.checkUsernameTaken("test")) { + authProvider.createAccountWith(details) + } + } + } + + @Test fun shouldRejectLoginWithInvalidDetails() { + Assertions.assertEquals(AuthResponse.InvalidCredentials, authProvider.checkLogin("test", "test2").first) + } + + @Test fun loginUsernameIsNotCaseSensitive() { + Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("Test", "testing").first) + Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("test", "testing").first) + } + + @Test fun shouldHashPasswords() { + Assertions.assertNotEquals("testing", storageProvider.getAccountInfo("test").password) + } + + @Test fun shouldNotAllowBannedLogin() { + val info = storageProvider.getAccountInfo("test") + info.banEndTime = System.currentTimeMillis() + 1000L + storageProvider.update(info) + Assertions.assertEquals(AuthResponse.AccountDisabled, authProvider.checkLogin("test", "testing").first) + info.banEndTime = 0L + storageProvider.update(info) + Assertions.assertEquals(AuthResponse.Success, authProvider.checkLogin("test", "testing").first) + } + + @Test fun shouldNotAllowAlreadyOnlineLogin() { + val info = storageProvider.getAccountInfo("test") + info.online = true + storageProvider.update(info) + Assertions.assertEquals(AuthResponse.AlreadyOnline, authProvider.checkLogin("test", "testing").first) + info.online = false + storageProvider.update(info) + } +} \ No newline at end of file diff --git a/Server/src/test/kotlin/core/net/LoginTests.kt b/Server/src/test/kotlin/core/net/LoginTests.kt new file mode 100644 index 000000000..a6ff5be50 --- /dev/null +++ b/Server/src/test/kotlin/core/net/LoginTests.kt @@ -0,0 +1,40 @@ +package core.net + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import rs09.auth.AuthResponse +import rs09.net.packet.`in`.Login +import java.nio.ByteBuffer + +class LoginTests { + @Test fun shouldDecodeCorrectlyEncodedPacket() { + val localCopy = validLoginPacket.copyOf(validLoginPacket.size) + val loginInfo = Login.decodeFromBuffer(ByteBuffer.wrap(localCopy)) + Assertions.assertEquals(AuthResponse.Success, loginInfo.first, "Info: ${loginInfo.second}") + } + + @Test fun shouldNeverThrowExceptionButReturnUnexpectedErrorResponseInstead() { + val localCopy = validLoginPacket.copyOf(validLoginPacket.size) + for(i in 15 until validLoginPacket.size) { + localCopy[i] = 0 //corrupt some data on purpose + } + var response: AuthResponse = AuthResponse.Updating + Assertions.assertDoesNotThrow { + val (res, _) = Login.decodeFromBuffer(ByteBuffer.wrap(localCopy)) + response = res + } + + Assertions.assertEquals(AuthResponse.UnexpectedError, response) + } + + @Test fun loginPacketWithInvalidOpcodeShouldReturnAHelpfulResponse() { + val localCopy = validLoginPacket.copyOf(validLoginPacket.size) + localCopy[0] = 2 //set to invalid login opcode + val (response, _) = Login.decodeFromBuffer(ByteBuffer.wrap(localCopy)) + Assertions.assertEquals(AuthResponse.InvalidLoginServer, response) + } + + companion object { + val validLoginPacket = byteArrayOf(16, 1, 80, 0, 0, 2, 18, 0, 1, 1, 2, 2, -3, 1, -9, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 107, 75, 109, 111, 107, 51, 107, 74, 113, 79, 101, 78, 54, 68, 51, 109, 68, 100, 105, 104, 99, 111, 51, 111, 80, 101, 89, 78, 50, 75, 70, 121, 54, 87, 53, 45, 45, 118, 90, 85, 98, 78, 65, 0, 0, 0, 0, 0, 1, 89, -73, -29, 0, 0, -47, -49, 34, 92, -124, 110, -97, 46, -33, -18, 122, 8, 76, 93, -82, 16, 53, 23, -113, -73, 101, 126, -93, 125, -65, 115, -59, 19, 90, -54, -102, -76, -99, -5, -68, -51, 95, -20, -27, 10, 60, 60, 108, 5, 76, 9, -63, 97, 106, 74, 116, 0, 58, 0, 75, -111, -128, -34, 12, 64, 47, -92, -33, -120, -109, -7, 23, 124, 122, 40, -107, 56, -34, -93, 64, -82, 58, -90, 7, 127, -15, -85, 125, 43, 15, 0, 112, 60, 4, 75, 72, 55, 18, 83, -119, -39, 32, -113, 21, 104, -49, 66, -102, 104, -13, 32, 117, -106, 94, -30, 37, -56, -67, 21, 77, 70, -128, 113, 86, 84, 83, 115, 3, 55, -106, 127, -14, -15, 6, 42, -92, -56, 114, 24, 83, 32, -127, 78, 14, -98, -30, -38, -115, 9, -106, 120, 101, -117, -50, -40, -64, -50, -106, -98, 5, -86, 28, -127, -5, 109, -107, 49, -107, 63, 81, -81, -109, -21, 25, -68, 63, 102, -5, 8, -96, 126, -128, -116, -32, 26, 76, 54, -63, -37, 41, 57, 65, -53, -22, 83, 61, -128, -17, -21, 87, 76, -8, -95, -45, -80, -60, -62, 96, 49, 26, 49, 94, 80, 11, -12, -95, -58, -116, -54, -107, 91, 104, 58, 33, 20, -93, -68, -83, -116, 63, -18, 36, 30, -98, 77, -107, 122, 79, -27, 67, -94, 125, -81, -21, -75, -71, -45, -39, 112, 40) + } +} \ No newline at end of file diff --git a/Server/src/test/kotlin/core/storage/SQLStorageProviderTests.kt b/Server/src/test/kotlin/core/storage/SQLStorageProviderTests.kt new file mode 100644 index 000000000..ab3895a29 --- /dev/null +++ b/Server/src/test/kotlin/core/storage/SQLStorageProviderTests.kt @@ -0,0 +1,181 @@ +package core.storage + +import core.auth.ProductionAuthenticatorTests +import org.junit.After +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import rs09.auth.UserAccountInfo +import rs09.game.system.SystemLogger +import rs09.storage.SQLStorageProvider +import java.sql.SQLDataException + +class SQLStorageProviderTests { + companion object { + val storage = SQLStorageProvider() + val testAccountNames = ArrayList() + init { + storage.configure("localhost", "global", "root", "") + val details = UserAccountInfo.createDefault() + details.rights = 2 + details.username = "test" + } + + @AfterAll @JvmStatic fun cleanup() { + SystemLogger.logInfo("Cleaning up unit test accounts") + testAccountNames.forEach {name -> + SystemLogger.logInfo("Removing test account $name") + val info = UserAccountInfo.createDefault() + info.username = name + storage.remove(info) + } + } + } + + @Test + fun shouldReturnTrueIfUsernameExists() { + val data = UserAccountInfo.createDefault() + data.username = "test123123" + data.password = "test" + testAccountNames.add("test123123") + storage.store(data) + val exists = storage.checkUsernameTaken("test") + Assertions.assertEquals(true, exists) + } + + @Test fun shouldReturnCorrectUserData() { + val data = UserAccountInfo.createDefault() + data.username = "test111" + data.password = "test" + data.rights = 2 + testAccountNames.add("test111") + storage.store(data) + val accountInfo = storage.getAccountInfo("test111") + Assertions.assertEquals(2, accountInfo.rights) + } + + @Test fun shouldAllowStoreUserData() { + val userData = UserAccountInfo.createDefault() + userData.username = "storageTest" + userData.password = "test" + testAccountNames.add("storageTest") + storage.store(userData) + val exists = storage.checkUsernameTaken("storageTest") + Assertions.assertEquals(true, exists) + } + + @Test fun shouldNotAllowDuplicateAccountStorage() { + val userData = UserAccountInfo.createDefault() + userData.username = "bilbo111" + userData.password = "test" + testAccountNames.add("bilbo111") + storage.store(userData) + Assertions.assertThrows(SQLDataException::class.java) { + storage.store(userData) + } + } + + @Test fun shouldAllowRemoveUserInfo() { + val userData = UserAccountInfo.createDefault() + userData.username = "bepis" + userData.password = "test" + testAccountNames.add("bepis") + storage.store(userData) + storage.remove(userData) + Assertions.assertEquals(false, storage.checkUsernameTaken("bepis")) + } + + @Test fun shouldUpdateUserData() { + val userData = UserAccountInfo.createDefault() + userData.username = "borpis" + userData.password = "test" + testAccountNames.add("borpis") + storage.store(userData) + userData.credits = 2 + storage.update(userData) + val data = storage.getAccountInfo(userData.username) + Assertions.assertEquals(2, data.credits, "Wrong data: $data") + } + + @Test fun shouldNotAllowStoreOrUpdateEmptyData() { + val info = UserAccountInfo.createDefault() + Assertions.assertThrows(IllegalStateException::class.java) { + storage.store(info) + } + Assertions.assertThrows(IllegalStateException::class.java) { + storage.update(info) + } + info.username = "test" + Assertions.assertThrows(IllegalStateException::class.java) { + storage.store(info) + } + Assertions.assertThrows(IllegalStateException::class.java) { + storage.update(info) + } + } + + @Test fun shouldSetDefaultValuesWhenDBFieldIsNull() { + val defaultData = UserAccountInfo.createDefault() + + //manually insert a definitely-mostly-null entry into the DB + val conn = storage.getConnection() + conn.use { + val stmt = conn.prepareStatement("INSERT INTO members (username) VALUES (?);") + stmt.setString(1, "nulltestacc") + testAccountNames.add("nulltestacc") + stmt.execute() + } + + var data: UserAccountInfo = UserAccountInfo.createDefault() + Assertions.assertDoesNotThrow { + data = storage.getAccountInfo("nulltestacc") + } + + Assertions.assertEquals(defaultData.password, data.password) + Assertions.assertEquals(defaultData.rights, data.rights) + Assertions.assertEquals(defaultData.credits, data.credits) + Assertions.assertEquals(defaultData.ip, data.ip) + Assertions.assertEquals(defaultData.lastUsedIp, data.lastUsedIp) + Assertions.assertEquals(defaultData.muteEndTime, data.muteEndTime) + Assertions.assertEquals(defaultData.banEndTime, data.banEndTime) + Assertions.assertEquals(defaultData.contacts, data.contacts) + Assertions.assertEquals(defaultData.blocked, data.blocked) + Assertions.assertEquals(defaultData.clanName, data.clanName) + Assertions.assertEquals(defaultData.currentClan, data.currentClan) + Assertions.assertEquals(defaultData.clanReqs, data.clanReqs) + Assertions.assertEquals(defaultData.timePlayed, data.timePlayed) + Assertions.assertEquals(defaultData.lastLogin, data.lastLogin) + Assertions.assertEquals(defaultData.online, data.online) + } + + @Test fun updatingPropertiesOnTheDatabaseEndShouldBePreservedWhenFetchingAccountInfo() { + val conn = storage.getConnection() + conn.use { + val stmt = conn.prepareStatement("INSERT INTO members (username) VALUES (?);") + stmt.setString(1, "dbupdateacc") + testAccountNames.add("dbupdateacc") + stmt.execute() + stmt.close() + + val stmt2 = conn.prepareStatement("UPDATE members SET rights = 2 WHERE username = \"dbupdateacc\";") + stmt2.execute() + } + + val info = storage.getAccountInfo("dbupdateacc") + Assertions.assertEquals(2, info.rights) + info.rights = 1 + storage.update(info) + + val info2 = storage.getAccountInfo("dbupdateacc") + Assertions.assertEquals(1, info2.rights) + + val conn2 = storage.getConnection() + conn2.use { + val stmt = conn2.prepareStatement("UPDATE members SET rights = 2 WHERE username = \"dbupdateacc\";") + stmt.execute() + } + + val info3 = storage.getAccountInfo("dbupdateacc") + Assertions.assertEquals(2, info3.rights) + } +} \ No newline at end of file