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