diff --git a/src/main/java/kinoko/database/AccountAccessor.java b/src/main/java/kinoko/database/AccountAccessor.java index 39af1563..3b0b60af 100644 --- a/src/main/java/kinoko/database/AccountAccessor.java +++ b/src/main/java/kinoko/database/AccountAccessor.java @@ -16,4 +16,8 @@ public interface AccountAccessor { boolean newAccount(String username, String password); boolean saveAccount(Account account); + + Optional getPinCode(int accountId); + + boolean savePinCode(int accountId, String pinCode); } diff --git a/src/main/java/kinoko/database/cassandra/CassandraAccountAccessor.java b/src/main/java/kinoko/database/cassandra/CassandraAccountAccessor.java index 32efdb43..820c0452 100644 --- a/src/main/java/kinoko/database/cassandra/CassandraAccountAccessor.java +++ b/src/main/java/kinoko/database/cassandra/CassandraAccountAccessor.java @@ -195,4 +195,34 @@ public boolean saveAccount(Account account) { ); return updateResult.wasApplied(); } + + @Override + public Optional getPinCode(int accountId) { + final ResultSet selectResult = getSession().execute( + selectFrom(getKeyspace(), AccountTable.getTableName()).all() + .column(AccountTable.PIN_CODE) + .whereColumn(AccountTable.ACCOUNT_ID).isEqualTo(literal(accountId)) + .build() + .setExecutionProfileName(CassandraConnector.PROFILE_ONE) + ); + final Row row = selectResult.one(); + if (row != null) { + final String pinCode = row.getString(AccountTable.PIN_CODE); + if (pinCode != null && !pinCode.isBlank()) { + return Optional.of(pinCode); + } + } + return Optional.empty(); + } + + @Override + public boolean savePinCode(int accountId, String pinCode) { + final ResultSet updateResult = getSession().execute( + update(getKeyspace(), AccountTable.getTableName()) + .setColumn(AccountTable.PIN_CODE, literal(pinCode)) + .whereColumn(AccountTable.ACCOUNT_ID).isEqualTo(literal(accountId)) + .build() + ); + return updateResult.wasApplied(); + } } diff --git a/src/main/java/kinoko/database/cassandra/table/AccountTable.java b/src/main/java/kinoko/database/cassandra/table/AccountTable.java index 18b0bf32..6f6fea2f 100644 --- a/src/main/java/kinoko/database/cassandra/table/AccountTable.java +++ b/src/main/java/kinoko/database/cassandra/table/AccountTable.java @@ -20,6 +20,7 @@ public final class AccountTable { public static final String TRUNK_MONEY = "trunk_money"; public static final String LOCKER_ITEMS = "locker_items"; public static final String WISHLIST = "wishlist"; + public static final String PIN_CODE = "pin_code"; private static final String tableName = "account_table"; @@ -44,6 +45,7 @@ public static void createTable(CqlSession session, String keyspace) { .withColumn(TRUNK_MONEY, DataTypes.INT) .withColumn(LOCKER_ITEMS, DataTypes.frozenListOf(SchemaBuilder.udt(CashItemInfoUDT.getTypeName(), true))) .withColumn(WISHLIST, DataTypes.frozenListOf(DataTypes.INT)) + .withColumn(PIN_CODE, DataTypes.TEXT) .build() ); session.execute( diff --git a/src/main/java/kinoko/handler/stage/LoginHandler.java b/src/main/java/kinoko/handler/stage/LoginHandler.java index ca425ef3..bad0e10d 100644 --- a/src/main/java/kinoko/handler/stage/LoginHandler.java +++ b/src/main/java/kinoko/handler/stage/LoginHandler.java @@ -2,8 +2,7 @@ import kinoko.database.DatabaseManager; import kinoko.handler.Handler; -import kinoko.packet.stage.LoginPacket; -import kinoko.packet.stage.LoginResultType; +import kinoko.packet.stage.*; import kinoko.provider.EtcProvider; import kinoko.provider.ItemProvider; import kinoko.provider.SkillProvider; @@ -82,7 +81,9 @@ public static void handleCheckPassword(Client c, InPacket inPacket) { c.setAccount(account); c.setMachineId(machineId); - c.getServerNode().addClient(c); + if (!ServerConfig.ENABLE_PIN_CODE) { + addClientToServerNode(c); + } c.write(LoginPacket.checkPasswordResultSuccess(account, c.getClientKey())); }); } @@ -415,6 +416,85 @@ public static void handleCheckSpwRequest(Client c, InPacket inPacket) { handleMigration(c, account, characterId); } + @Handler(InHeader.CheckPinCode) + public static void handleCheckPinCode(Client c, InPacket inPacket) { + final byte pinCodeModalOpt = inPacket.decodeByte(); + if (pinCodeModalOpt == PinCodeModalOpt.CANCEL.getValue()) { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.Cancel)); + return; + } + + final byte pinCodeAttemptMaxCount = 5; + + final boolean pinCodeShouldRegisterOrEnterInLoginOpt = inPacket.decodeBoolean(); + + final String inputPinCode = inPacket.decodeString(); + if (pinCodeModalOpt == PinCodeModalOpt.LOGIN.getValue()) { + final Optional pinCodeOptional = DatabaseManager.accountAccessor().getPinCode(c.getAccount().getId()); + if (pinCodeShouldRegisterOrEnterInLoginOpt) { + if (pinCodeOptional.isEmpty() || pinCodeOptional.get().isBlank()) { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CreateOrUpdate)); + } else { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.RequestToEnter)); + } + } else { + if (pinCodeOptional.isEmpty() || pinCodeOptional.get().isBlank()) { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CreateOrUpdate)); + } else { + if (inputPinCode.equals(pinCodeOptional.get())) { + c.setPinCodeAttemptCount(0); + addClientToServerNode(c); + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.Done)); + } else { + final int pinCodeAttemptCount = c.getPinCodeAttemptCount(); + if (pinCodeAttemptCount >= pinCodeAttemptMaxCount) { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CheckTooMuchInvalid)); + } else { + c.setPinCodeAttemptCount(pinCodeAttemptCount + 1); + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CheckInvalid)); + } + } + } + } + } else if (pinCodeModalOpt == PinCodeModalOpt.CHANGE_PIN.getValue()) { + final Optional pinCodeOptional = DatabaseManager.accountAccessor().getPinCode(c.getAccount().getId()); + if (pinCodeOptional.isPresent() && inputPinCode.equals(pinCodeOptional.get())) { + c.setPinCodeAttemptCount(0); + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CreateOrUpdate)); + } else { + final int pinCodeAttemptCount = c.getPinCodeAttemptCount(); + if (pinCodeAttemptCount >= pinCodeAttemptMaxCount) { + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CheckTooMuchInvalid)); + } else { + c.setPinCodeAttemptCount(pinCodeAttemptCount + 1); + c.write(LoginPacket.checkPinCodeResult(CheckPinCodeResultType.CheckInvalid)); + } + } + } + } + + @Handler(InHeader.UpdatePinCode) + public static void handleUpdatePinCode(Client c, InPacket inPacket) { +// void __thiscall CLogin::OnUpdatePinCodeResult(CLogin *this, CInPacket *iPacket) +// { +// if ( CInPacket::Decode1(iPacket) ) +// CLoginUtilDlg::Error(15, 0); +// else +// CPinCodeDlg::Notice(8); +// CUITitle::EnableLoginCtrl((CUITitle *)TSingleton::ms_pInstance._m_pStr, 1); +// } + final byte pinCodeUpdateModalOpt = inPacket.decodeByte(); + boolean hasErrorInUpdate = false; + if (pinCodeUpdateModalOpt == PinCodeUpdateModalOpt.CANCEL.getValue()) { + // If the user clicks the cancel button, then also set hasErrorInUpdate to true. + hasErrorInUpdate = true; + } else { + final String pinCode = inPacket.decodeString(); + hasErrorInUpdate = !DatabaseManager.accountAccessor().savePinCode(c.getAccount().getId(), pinCode); + } + c.write(LoginPacket.updatePinCodeResult(hasErrorInUpdate)); + } + private static void loadCharacterList(Client c) { // Resolve character list for account, sorted by highest level final Account account = c.getAccount(); @@ -452,4 +532,8 @@ private static void handleMigration(Client c, Account account, int characterId) c.write(LoginPacket.selectCharacterResultSuccess(transferInfo.getChannelHost(), transferInfo.getChannelPort(), characterId)); }); } + + public static void addClientToServerNode(Client c) { + c.getServerNode().addClient(c); + } } diff --git a/src/main/java/kinoko/packet/stage/CheckPinCodeResultType.java b/src/main/java/kinoko/packet/stage/CheckPinCodeResultType.java new file mode 100644 index 00000000..0700441b --- /dev/null +++ b/src/main/java/kinoko/packet/stage/CheckPinCodeResultType.java @@ -0,0 +1,23 @@ +package kinoko.packet.stage; + +public enum CheckPinCodeResultType { + Cancel(-1), // This value does not exist on the client side; it is only used on the server side to indicate a cancellation operation. + Done(0), // In the client, when CheckPinCodeResultType =0 and m_nGameStartMode != 1, a WorldRequest packet will be sent. + CreateOrUpdate(1), + CheckInvalid(2), + CheckTooMuchInvalid(3), + RequestToEnter(4), + // no have 5,6 value + AccountAlreadyLogged(7), + ; + + private final int value; + + CheckPinCodeResultType(int value) { + this.value = value; + } + + public final int getValue() { + return value; + } +} diff --git a/src/main/java/kinoko/packet/stage/LoginPacket.java b/src/main/java/kinoko/packet/stage/LoginPacket.java index bd921fe7..55d526ef 100644 --- a/src/main/java/kinoko/packet/stage/LoginPacket.java +++ b/src/main/java/kinoko/packet/stage/LoginPacket.java @@ -46,7 +46,7 @@ public static OutPacket checkPasswordResultSuccess(Account account, byte[] clien outPacket.encodeLong(0); // dtRegisterDate outPacket.encodeInt(account.getSlotCount()); // nNumOfCharacter - outPacket.encodeByte(true); // true ? VIEW_WORLD_SELECT : CHECK_PIN_CODE + outPacket.encodeByte(!ServerConfig.ENABLE_PIN_CODE); // true ? WORLD_REQUEST : CHECK_PIN_CODE outPacket.encodeByte(LoginOpt.getLoginOpt(account).getValue()); // bLoginOpt outPacket.encodeArray(clientKey); return outPacket; @@ -206,6 +206,18 @@ public static OutPacket checkSecondaryPasswordResult() { return outPacket; } + public static OutPacket checkPinCodeResult(CheckPinCodeResultType resultType) { + final OutPacket outPacket = OutPacket.of(OutHeader.CheckPinCodeResult); + outPacket.encodeByte(resultType.getValue()); + return outPacket; + } + + public static OutPacket updatePinCodeResult(boolean updateResult) { + final OutPacket outPacket = OutPacket.of(OutHeader.UpdatePinCodeResult); + outPacket.encodeByte(updateResult); + return outPacket; + } + private enum LoginOpt { INITIALIZE_SECONDARY_PASSWORD(0), CHECK_SECONDARY_PASSWORD(1), diff --git a/src/main/java/kinoko/packet/stage/PinCodeModalOpt.java b/src/main/java/kinoko/packet/stage/PinCodeModalOpt.java new file mode 100644 index 00000000..ff3312a3 --- /dev/null +++ b/src/main/java/kinoko/packet/stage/PinCodeModalOpt.java @@ -0,0 +1,17 @@ +package kinoko.packet.stage; + +public enum PinCodeModalOpt { + CANCEL(0), + LOGIN(1), + CHANGE_PIN(2), + ; + private final int value; + + PinCodeModalOpt(int value) { + this.value = value; + } + + public final int getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/main/java/kinoko/packet/stage/PinCodeUpdateModalOpt.java b/src/main/java/kinoko/packet/stage/PinCodeUpdateModalOpt.java new file mode 100644 index 00000000..ac9c29c7 --- /dev/null +++ b/src/main/java/kinoko/packet/stage/PinCodeUpdateModalOpt.java @@ -0,0 +1,16 @@ +package kinoko.packet.stage; + +public enum PinCodeUpdateModalOpt { + CANCEL(0), + OK(1), + ; + private final int value; + + PinCodeUpdateModalOpt(int value) { + this.value = value; + } + + public final int getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/main/java/kinoko/server/ServerConfig.java b/src/main/java/kinoko/server/ServerConfig.java index e67eaf70..415a9127 100644 --- a/src/main/java/kinoko/server/ServerConfig.java +++ b/src/main/java/kinoko/server/ServerConfig.java @@ -13,6 +13,7 @@ public final class ServerConfig { public static final boolean AUTO_CREATE_ACCOUNT = Util.getEnv("AUTO_CREATE_ACCOUNT", true); public static final boolean REQUIRE_SECONDARY_PASSWORD = Util.getEnv("REQUIRE_SECONDARY_PASSWORD", true); + public static final boolean ENABLE_PIN_CODE = Util.getEnv("ENABLE_PIN_CODE", true); public static final String WZ_DIRECTORY = Util.getEnv("WZ_DIRECTORY", "wz"); public static final String DATA_DIRECTORY = Util.getEnv("DATA_DIRECTORY", "data"); diff --git a/src/main/java/kinoko/server/node/Client.java b/src/main/java/kinoko/server/node/Client.java index 8d70dd4b..5b962e8f 100644 --- a/src/main/java/kinoko/server/node/Client.java +++ b/src/main/java/kinoko/server/node/Client.java @@ -12,9 +12,19 @@ public final class Client extends NettyClient { private User user; private byte[] machineId; private byte[] clientKey; + private int pinCodeAttemptCount; public Client(ServerNode serverNode, SocketChannel socketChannel) { super(serverNode, socketChannel); + this.pinCodeAttemptCount = 0; + } + + public int getPinCodeAttemptCount() { + return pinCodeAttemptCount; + } + + public void setPinCodeAttemptCount(int pinCodeAttemptCount) { + this.pinCodeAttemptCount = pinCodeAttemptCount; } public Account getAccount() {