Introduced modular components for authentication, including the storage backend

Servers in dev mode now have a no-auth equivalent that allows any user/pass combo without registration
Added a ban command
Added a mute command
Hooked up the mute functionality of the report screen (for pmods+)
Cleaned up all the now-unused classes for player SQL stuff
Player SQL stuff now uses entirely prepared statements
No longer storing PC name, MAC address, serial number as these are inauthentic components of the protocol Packet to be corrected in the future to allow closer compatibility with authentic clients
Used less threading for the SQL queries/updates as these were causing issues both with the old system and the new
Updated ::resetpassword and ::setpasswordother commands to use the new server authentication pipeline (to ensure things are always correctly set)
Refactored the login read event, now handles more exceptions and edge cases
This commit is contained in:
Ceikry 2022-05-16 11:51:42 +00:00 committed by Ryan
parent dd1eed7e39
commit 9ab9885eef
41 changed files with 1264 additions and 1424 deletions

View file

@ -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.");
}

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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? <br>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);

View file

@ -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;
}
}

View file

@ -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 <T> The entry type.
*/
public abstract class SQLEntryHandler<T> {
/**
* 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();
}

View file

@ -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<Player> {
/**
* 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();
}
}

View file

@ -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<PlayerMonitor> {
/**
* 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<Integer> 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();
}
}

View file

@ -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<PlayerDetails> {
/**
* 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<SQLColumn> 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();
}
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
});

View file

@ -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);

View file

@ -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;
}

View file

@ -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<RegistryDetails> {
/**
* 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<RegistryDetails> {
*/
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<RegistryDetails> {
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<RegistryDetails> {
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<RegistryDetails> {
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<RegistryDetails> {
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<RegistryDetails> {
}
}
@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<RegistryDetails> {
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();
}
}

View file

@ -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;
}
}

View file

@ -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<String, Connection> connections = new HashMap<>();
private Map<String, Database> 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<String, Connection> connections() {
return connections;
}
public Map<String, Database> databases() {
return databases;
}
public boolean isConnected() {
return connected;
}
}

View file

@ -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;

View file

@ -0,0 +1,22 @@
package rs09.auth
import core.game.node.entity.player.Player
import rs09.storage.AccountStorageProvider
abstract class AuthProvider<T: AccountStorageProvider> {
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<AuthResponse,UserAccountInfo?>
abstract fun checkPassword(player: Player, password: String) : Boolean
abstract fun updatePassword(username: String, newPassword: String)
}

View file

@ -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
}

View file

@ -0,0 +1,39 @@
package rs09.auth
import core.game.node.entity.player.Player
import rs09.storage.InMemoryStorageProvider
class DevelopmentAuthenticator : AuthProvider<InMemoryStorageProvider>() {
override fun configureFor(provider: InMemoryStorageProvider) {
storageProvider = provider
}
override fun checkLogin(username: String, password: String): Pair<AuthResponse, UserAccountInfo?> {
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)
}
}

View file

@ -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<AccountStorageProvider>() {
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<AuthResponse, UserAccountInfo?> {
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)
}
}

View file

@ -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
}
}

View file

@ -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."

View file

@ -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 {

View file

@ -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")
}
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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() }

View file

@ -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()

View file

@ -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)
}
}

View file

@ -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<AuthResponse, LoginInfo?> {
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)))
}
}

View file

@ -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"
}
}

View file

@ -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)
}

View file

@ -0,0 +1,27 @@
package rs09.storage
import rs09.auth.UserAccountInfo
class InMemoryStorageProvider : AccountStorageProvider {
private val storage = HashMap<String, UserAccountInfo>()
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)
}
}

View file

@ -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 = ?;"
}
}

View file

@ -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)

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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<String>()
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)
}
}