diff --git a/src/main/java/client/Client.java b/src/main/java/client/Client.java index 0b49c514dc..50fa5de086 100644 --- a/src/main/java/client/Client.java +++ b/src/main/java/client/Client.java @@ -22,6 +22,7 @@ along with this program. If not, see . package client; import config.YamlConfig; +import database.account.Account; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.timeout.IdleStateEvent; @@ -63,6 +64,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; +import java.time.LocalDate; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; @@ -72,6 +74,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.concurrent.Semaphore; import java.util.concurrent.locks.Lock; @@ -84,6 +87,8 @@ public class Client extends ChannelInboundHandlerAdapter { private static final int MAX_FAILED_LOGIN_ATTEMPTS = 5; private static final int MAX_CHR_SLOTS = 15; + public static final byte NO_GENDER = 10; + public static final int LOGIN_NOTLOGGEDIN = 0; public static final int LOGIN_SERVER_TRANSITION = 1; public static final int LOGIN_LOGGEDIN = 2; @@ -97,12 +102,13 @@ public class Client extends ChannelInboundHandlerAdapter { private volatile boolean inTransition; private io.netty.channel.Channel ioChannel; + private Account account; private Character player; private int channel = 1; private int accId = -4; private boolean loggedIn = false; private boolean serverTransition = false; - private Calendar birthday = null; + private Calendar birthday = null; // TODO: convert to LocalDate private String accountName = null; private int world; private volatile long lastPong; @@ -281,6 +287,25 @@ public class Client extends ChannelInboundHandlerAdapter { return getChannelServer().getEventSM().getEventManager(event); } + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + Objects.requireNonNull(account); + this.account = account; + this.accId = account.id(); + this.accountName = account.name(); + this.characterSlots = account.chrSlots(); + this.pin = account.pin(); + this.pic = account.pic(); + this.gender = Objects.requireNonNullElse(account.gender(), NO_GENDER); + Calendar calendar = Calendar.getInstance(); + LocalDate birthdate = account.birthdate(); + calendar.set(birthdate.getYear(), birthdate.getMonthValue() - 1, birthdate.getDayOfMonth()); + this.birthday = calendar; + } + public Character getPlayer() { return player; } @@ -301,6 +326,8 @@ public class Client extends ChannelInboundHandlerAdapter { return serverTransition; } + // TODO: load ipbans on server start and query it on demand. This query should not be run on every login! + @Deprecated public boolean hasBannedIP() { boolean ret = false; try (Connection con = DatabaseConnection.getConnection(); @@ -342,6 +369,8 @@ public class Client extends ChannelInboundHandlerAdapter { return ret; } + // TODO: load macbans on server start and query it on demand. This query should not be run on every login! + @Deprecated public boolean hasBannedMac() { if (macs.isEmpty()) { return false; @@ -377,6 +406,7 @@ public class Client extends ChannelInboundHandlerAdapter { } // TODO: Recode to close statements... + // Only used from ban command. private void loadMacsIfNescessary() throws SQLException { if (macs.isEmpty()) { try (Connection con = DatabaseConnection.getConnection(); @@ -493,15 +523,21 @@ public class Client extends ChannelInboundHandlerAdapter { return false; } - public int login(String login, String pwd, Hwid hwid) { - int loginok = 5; - + public boolean tryLogin() { if (++loginattempt >= MAX_FAILED_LOGIN_ATTEMPTS) { loggedIn = false; SessionCoordinator.getInstance().closeSession(this, false); - return 6; // thanks Survival_Project for finding out an issue with AUTOMATIC_REGISTER here + return false; } + return true; + } + + // TODO: load account outside in LoginPasswordHandler (from service). + // + public int login(String login, String pwd, Hwid hwid) { + int loginok = 5; + try (Connection con = DatabaseConnection.getConnection(); PreparedStatement ps = con.prepareStatement("SELECT id, password, gender, banned, pin, pic, characterslots, tos FROM accounts WHERE name = ?")) { ps.setString(1, login); @@ -576,6 +612,8 @@ public class Client extends ChannelInboundHandlerAdapter { } } + // TODO: check tempban directly on loaded account + @Deprecated public Calendar getTempBanCalendarFromDB() { final Calendar lTempban = Calendar.getInstance(); @@ -675,6 +713,7 @@ public class Client extends ChannelInboundHandlerAdapter { } } + // TODO: move to LoginPasswordHandler public int getLoginState() { // 0 = LOGIN_NOTLOGGEDIN, 1= LOGIN_SERVER_TRANSITION, 2 = LOGIN_LOGGEDIN try (Connection con = DatabaseConnection.getConnection()) { int state; @@ -952,6 +991,7 @@ public class Client extends ChannelInboundHandlerAdapter { public void setGender(byte m) { this.gender = m; + // TODO: move to AccountService try (Connection con = DatabaseConnection.getConnection(); PreparedStatement ps = con.prepareStatement("UPDATE accounts SET gender = ? WHERE id = ?")) { ps.setByte(1, gender); diff --git a/src/main/java/database/account/Account.java b/src/main/java/database/account/Account.java index 3051d2d796..c55f827d5d 100644 --- a/src/main/java/database/account/Account.java +++ b/src/main/java/database/account/Account.java @@ -4,11 +4,18 @@ import lombok.Builder; import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.Objects; /** * @author Ponk */ @Builder -public record Account(int id, String name, String password, boolean acceptedTos, byte gender, LocalDate birthdate, - String pin, String pic, int chrSlots, int loggedIn, LocalDateTime lastLogin, boolean banned) { +public record Account(int id, String name, String password, boolean acceptedTos, Byte gender, LocalDate birthdate, + String pin, String pic, byte chrSlots, byte loginState, LocalDateTime lastLogin, boolean banned, + LocalDateTime tempBanTimestamp) { + public Account { + Objects.requireNonNull(name); + Objects.requireNonNull(password); + Objects.requireNonNull(birthdate); + } } diff --git a/src/main/java/database/account/AccountRepository.java b/src/main/java/database/account/AccountRepository.java index a8ad89c290..5a9ee7f035 100644 --- a/src/main/java/database/account/AccountRepository.java +++ b/src/main/java/database/account/AccountRepository.java @@ -15,9 +15,24 @@ public class AccountRepository { this.connection = connection; } + public Optional findByNameIgnoreCase(String name) { + String sql = """ + SELECT id, name, password, pin, pic, birthdate, gender, tos_accepted, chr_slots, login_state, + last_login, banned, temp_ban_timestamp + FROM account + WHERE lower(name) = lower(:name)"""; + try (Handle handle = connection.getHandle()) { + return handle.createQuery(sql) + .bind("name", name) + .mapTo(Account.class) + .findOne(); + } + } + public Optional findById(int accountId) { String sql = """ - SELECT id, name, password, pin, pic, logged_in, last_login, birthdate, banned, gender, tos_accepted + SELECT id, name, password, pin, pic, birthdate, gender, tos_accepted, chr_slots, login_state, + last_login, banned, temp_ban_timestamp FROM account WHERE id = :id"""; try (Handle handle = connection.getHandle()) { @@ -30,13 +45,15 @@ public class AccountRepository { public Integer insert(Account account) { String sql = """ - INSERT INTO account (name, password, birthdate) - VALUES (:name, :password, :birthdate)"""; + INSERT INTO account (name, password, birthdate, chr_slots, login_state) + VALUES (:name, :password, :birthdate, :chrSlots, :loginState)"""; try (Handle handle = connection.getHandle()) { return handle.createUpdate(sql) .bind("name", account.name()) .bind("password", account.password()) .bind("birthdate", account.birthdate()) + .bind("chrSlots", account.chrSlots()) + .bind("loginState", account.loginState()) .executeAndReturnGeneratedKeys("id") .mapTo(Integer.class) .one(); diff --git a/src/main/java/database/account/AccountRowMapper.java b/src/main/java/database/account/AccountRowMapper.java index 101fb7b9b6..c451fc6379 100644 --- a/src/main/java/database/account/AccountRowMapper.java +++ b/src/main/java/database/account/AccountRowMapper.java @@ -21,14 +21,20 @@ public class AccountRowMapper implements RowMapper { .password(rs.getString("password")) .pin(rs.getString("pin")) .pic(rs.getString("pic")) - .loggedIn(rs.getInt("logged_in")) + .birthdate(rs.getDate("birthdate").toLocalDate()) + .gender(Optional.ofNullable(rs.getObject("gender", Short.class)) + .map(Short::byteValue) + .orElse(null)) + .acceptedTos(rs.getBoolean("tos_accepted")) + .chrSlots(rs.getByte("chr_slots")) + .loginState(rs.getByte("login_state")) .lastLogin(Optional.ofNullable(rs.getTimestamp("last_login")) .map(Timestamp::toLocalDateTime) .orElse(null)) - .birthdate(rs.getDate("birthdate").toLocalDate()) .banned(rs.getBoolean("banned")) - .gender(rs.getByte("gender")) - .acceptedTos(rs.getBoolean("tos_accepted")) + .tempBanTimestamp(Optional.ofNullable(rs.getTimestamp("temp_ban_timestamp")) + .map(Timestamp::toLocalDateTime) + .orElse(null)) .build(); } } diff --git a/src/main/java/net/server/handlers/login/LoginPasswordHandler.java b/src/main/java/net/server/handlers/login/LoginPasswordHandler.java index 40bae25cfd..da9a4bcb37 100644 --- a/src/main/java/net/server/handlers/login/LoginPasswordHandler.java +++ b/src/main/java/net/server/handlers/login/LoginPasswordHandler.java @@ -26,10 +26,12 @@ import client.Client; import client.DefaultDates; import config.YamlConfig; import constants.game.GameConstants; +import database.account.Account; import net.PacketHandler; import net.packet.InPacket; import net.server.Server; import net.server.coordinator.session.Hwid; +import net.server.coordinator.session.SessionCoordinator; import net.server.world.World; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,6 +48,7 @@ import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Timestamp; import java.util.Calendar; +import java.util.Optional; public final class LoginPasswordHandler implements PacketHandler { private static final Logger log = LoggerFactory.getLogger(LoginPasswordHandler.class); @@ -64,7 +67,7 @@ public final class LoginPasswordHandler implements PacketHandler { } @Override - public final void handlePacket(InPacket p, Client c) { + public void handlePacket(InPacket p, Client c) { String remoteHost = c.getRemoteAddress(); if (remoteHost.contentEquals("null")) { c.sendPacket(PacketCreator.getLoginFailed(14)); // thanks Alchemist for noting remoteHost could be null @@ -73,51 +76,94 @@ public final class LoginPasswordHandler implements PacketHandler { String login = p.readString(); String pwd = p.readString(); - c.setAccountName(login); p.skip(6); // localhost masked the initial part with zeroes... byte[] hwidNibbles = p.readBytes(4); - Hwid hwid = new Hwid(HexTool.toCompactHexString(hwidNibbles)); - int loginok = c.login(login, pwd, hwid); + c.setHwid(new Hwid(HexTool.toCompactHexString(hwidNibbles))); - - if (YamlConfig.config.server.AUTOMATIC_REGISTER && loginok == 5) { - try { - int accountId = createAccountPostgres(login, pwd); - createAccountMysql(accountId, login, pwd); - c.setAccID(accountId); - } finally { - loginok = c.login(login, pwd, hwid); + if (!c.tryLogin()) { + return; + } + Optional foundAccount = accountService.getAccount(login); + if (foundAccount.isEmpty()) { + if (YamlConfig.config.server.AUTOMATIC_REGISTER) { + Account newAccount = createAccount(login, pwd); + foundAccount = Optional.of(newAccount); + } else { + c.sendPacket(PacketCreator.getLoginFailed(5)); + return; } } - if (c.hasBannedIP() || c.hasBannedMac()) { + Account account = foundAccount.get(); + if (account.banned()) { + c.sendPacket(PacketCreator.getLoginFailed(3)); + // TODO: send ban reason instead of login failed, something like this: + // c.sendPacket(PacketCreator.getPermBan(c.getGReason())); + return; + } + + if (!correctPassword(pwd, account)) { + c.sendPacket(PacketCreator.getLoginFailed(4)); + return; + } + + c.setAccount(account); + + if (c.getLoginState() > Client.LOGIN_NOTLOGGEDIN) { + c.sendPacket(PacketCreator.getLoginFailed(7)); + return; + } + + if (!account.acceptedTos()) { + c.sendPacket(PacketCreator.getLoginFailed(23)); + return; + } + + boolean banCheckDisabled = false; + if (!banCheckDisabled && (c.hasBannedIP() || c.hasBannedMac())) { c.sendPacket(PacketCreator.getLoginFailed(3)); return; } - Calendar tempban = c.getTempBanCalendarFromDB(); - if (tempban != null) { + + /* TODO: check temp ban from account, something like this: + LocalDateTime tempBan = account.tempBanTimestamp(); + if (tempBan != null && tempBan.isAfter(LocalDateTime.now())) { + Duration remainingTempBan = Duration.between(LocalDateTime.now(), tempBan); + c.sendPacket(PacketCreator.getTempBan()); + } + */ + boolean tempBanDisabled = false; + Calendar tempban = null; + if (!tempBanDisabled && (tempban = c.getTempBanCalendarFromDB()) != null) { if (tempban.getTimeInMillis() > Calendar.getInstance().getTimeInMillis()) { c.sendPacket(PacketCreator.getTempBan(tempban.getTimeInMillis(), c.getGReason())); return; } } - if (loginok == 3) { - c.sendPacket(PacketCreator.getPermBan(c.getGReason()));//crashes but idc :D - return; - } else if (loginok != 0) { - c.sendPacket(PacketCreator.getLoginFailed(loginok)); + + Integer failureCode = checkMultiClient(c); + if (failureCode != null) { + c.sendPacket(PacketCreator.getLoginFailed(failureCode)); return; } - if (c.finishLogin()) { - checkChar(c); - login(c); - } else { + + if (!c.finishLogin()) { c.sendPacket(PacketCreator.getLoginFailed(7)); } + checkChar(c); + + c.sendPacket(PacketCreator.getAuthSuccess(c)); + Server.getInstance().registerLoginState(c); } - private int createAccountPostgres(String name, String password) { + private Account createAccount(String name, String password) { + Account account = createAccountPostgres(name, password); + createAccountMysql(account.id(), name, password); + return account; + } + + private Account createAccountPostgres(String name, String password) { return accountService.createNew(name, password); } @@ -126,7 +172,7 @@ public final class LoginPasswordHandler implements PacketHandler { PreparedStatement ps = con.prepareStatement("INSERT INTO accounts (id, name, password, birthday, tempban) VALUES (?, ?, ?, ?, ?);")) { ps.setInt(1, id); ps.setString(2, name); - ps.setString(3, BCrypt.hashpw(password, BCrypt.gensalt(12))); + ps.setString(3, BCrypt.hashpw(password, BCrypt.gensalt())); ps.setDate(4, Date.valueOf(DefaultDates.getBirthday())); ps.setTimestamp(5, Timestamp.valueOf(DefaultDates.getTempban())); ps.executeUpdate(); @@ -135,6 +181,24 @@ public final class LoginPasswordHandler implements PacketHandler { } } + private boolean correctPassword(String input, Account account) { + return BCrypt.checkpw(input, account.password()); + } + + private Integer checkMultiClient(Client c) { + SessionCoordinator.AntiMulticlientResult res = SessionCoordinator.getInstance() + .attemptLoginSession(c, c.getHwid(), c.getAccID(), false); + + return switch (res) { + case SUCCESS -> null; + case REMOTE_LOGGEDIN -> 17; + case REMOTE_REACHED_LIMIT -> 13; + case REMOTE_PROCESSING -> 10; + case MANY_ACCOUNT_ATTEMPTS -> 16; + default -> 8; + }; + } + private void checkChar(Client c) { // issue with multiple chars from same account login found by shavit, resinate if (!YamlConfig.config.server.USE_CHARACTER_ACCOUNT_CHECK) { return; @@ -151,9 +215,4 @@ public final class LoginPasswordHandler implements PacketHandler { } } } - - private static void login(Client c) { - c.sendPacket(PacketCreator.getAuthSuccess(c));//why the fk did I do c.getAccountName()? - Server.getInstance().registerLoginState(c); - } } diff --git a/src/main/java/net/server/handlers/login/SetGenderHandler.java b/src/main/java/net/server/handlers/login/SetGenderHandler.java index 255dd19c84..7dcef4b701 100644 --- a/src/main/java/net/server/handlers/login/SetGenderHandler.java +++ b/src/main/java/net/server/handlers/login/SetGenderHandler.java @@ -35,7 +35,7 @@ import tools.PacketCreator; public class SetGenderHandler extends AbstractPacketHandler { @Override public void handlePacket(InPacket p, Client c) { - if (c.getGender() == 10) { //Packet shouldn't come if Gender isn't 10. + if (c.getGender() == Client.NO_GENDER) { //Packet shouldn't come if Gender isn't 10. byte confirmed = p.readByte(); if (confirmed == 0x01) { c.setGender(p.readByte()); diff --git a/src/main/java/service/AccountService.java b/src/main/java/service/AccountService.java index a7d3141945..7d59d940ea 100644 --- a/src/main/java/service/AccountService.java +++ b/src/main/java/service/AccountService.java @@ -19,8 +19,8 @@ import java.util.Optional; */ @Slf4j public class AccountService { - private static final int PASSWORD_HASH_SALT_LOG_ROUNDS = 12; private static final LocalDate GMS_RELEASE = LocalDate.of(2005, 5, 11); + private static final byte INITIAL_CHR_SLOTS = 3; private final AccountRepository accountRepository; @@ -28,27 +28,34 @@ public class AccountService { this.accountRepository = accountRepository; } - public int createNew(String name, String password) { + public Account createNew(String name, String password) { Account newAccount = Account.builder() .name(name) .password(hashPassword(password)) .birthdate(GMS_RELEASE) + .chrSlots(INITIAL_CHR_SLOTS) + .loginState((byte) Client.LOGIN_NOTLOGGEDIN) + .gender(null) .build(); Integer accountId; try { accountId = accountRepository.insert(newAccount); } catch (Exception e) { - log.error("Failed to create new account", e); - throw new RuntimeException("Failed to create new account"); + log.error("Failed to insert new account", e); + throw new RuntimeException("Failed to insert new account"); } - return Optional.ofNullable(accountId) - .orElseThrow(() -> new RuntimeException("Failed to create new account - missing id")); + return getAccount(accountId) + .orElseThrow(() -> new RuntimeException("Failed to get account after insert, id: " + accountId)); } private String hashPassword(String password) { - return BCrypt.hashpw(password, BCrypt.gensalt(PASSWORD_HASH_SALT_LOG_ROUNDS)); + return BCrypt.hashpw(password, BCrypt.gensalt()); + } + + public Optional getAccount(String name) { + return accountRepository.findByNameIgnoreCase(name); } public Optional getAccount(int accountId) { diff --git a/src/main/resources/db/migration/postgresql/V0.2__account.sql b/src/main/resources/db/migration/postgresql/V0.2__account.sql index ab4c8cbbd6..2725a78e2c 100644 --- a/src/main/resources/db/migration/postgresql/V0.2__account.sql +++ b/src/main/resources/db/migration/postgresql/V0.2__account.sql @@ -5,23 +5,23 @@ CREATE TABLE account password varchar(200) NOT NULL, pin varchar(4), pic varchar(26), - logged_in smallint DEFAULT 0 NOT NULL, created_at timestamp DEFAULT now() NOT NULL, - last_login timestamp, birthdate date NOT NULL, - banned boolean DEFAULT false NOT NULL, - banreason text, - macs text, + tos_accepted boolean DEFAULT false NOT NULL, + gender smallint, + chr_slots smallint NOT NULL, nx_credit integer DEFAULT 0 NOT NULL, maple_point integer DEFAULT 0 NOT NULL, nx_prepaid integer DEFAULT 0 NOT NULL, - chr_slots smallint DEFAULT 3 NOT NULL, - gender smallint DEFAULT 10 NOT NULL, + login_state smallint NOT NULL, + last_login timestamp, + banned boolean DEFAULT false NOT NULL, + banreason text, temp_ban_timestamp timestamp, greason smallint, - tos_accepted boolean DEFAULT false NOT NULL, ip text, hwid text, + macs text, PRIMARY KEY (id), UNIQUE (name) );