/*
* To change this template, choose Tools | Templates and open the template in
* the editor.
*/
package javastory.login;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.List;
import javastory.channel.client.MemberRank;
import javastory.client.GameCharacterUtil;
import javastory.client.GameClient;
import javastory.client.LoginCrypto;
import javastory.cryptography.AesTransform;
import javastory.db.Database;
import javastory.game.Gender;
import javastory.game.Item;
import javastory.game.Inventory;
import javastory.io.PacketFormatException;
import javastory.io.PacketReader;
import javastory.tools.FiletimeUtil;
import javastory.tools.packets.CommonPackets;
import org.apache.mina.core.session.IoSession;
import com.google.common.collect.Lists;
/**
*
* @author shoftee
*/
public final class LoginClient extends GameClient {
private static final int LOGIN_ATTEMPTS = 5;
//
private String loginPassword, loginPasswordSalt;
private String characterPassword, characterPasswordSalt;
private Calendar temporaryBan = null;
private boolean isGm;
private byte temporaryBanReason = 1;
private Gender gender;
private int characterSlots = 0;
//
private int loginAttempt = 0;
private final List<Integer> allowedChar = Lists.newLinkedList();
public LoginClient(final AesTransform clientCrypto, final AesTransform serverCrypto, final IoSession session) {
super(clientCrypto, serverCrypto, session);
this.loggedIn = false;
}
public boolean hasBannedIP() {
final Connection con = Database.getConnection();
try ( PreparedStatement ps = getCountIpBans(con);
ResultSet rs = ps.executeQuery()) {
rs.next();
final boolean hasBans = rs.getInt(1) > 0;
return hasBans;
} catch (final SQLException ex) {
System.err.println("Error checking ip bans" + ex);
return false;
}
}
private PreparedStatement getCountIpBans(final Connection con) throws SQLException {
PreparedStatement ps = con.prepareStatement("SELECT COUNT(*) FROM ipbans WHERE ? LIKE CONCAT(ip, '%')");
ps.setString(1, super.getSessionIP());
return ps;
}
public final boolean deleteCharacter(final int characterId) {
final Connection con = Database.getConnection();
// TODO: Dude, transactions!
try {
try ( final PreparedStatement ps = getSelectGuildInfo(con, characterId);
final ResultSet rs = ps.executeQuery()) {
if (!rs.next()) {
return false;
}
if (rs.getInt("guildid") > 0) {
// is in a guild when deleted
if (MemberRank.fromNumber(rs.getInt("guildrank")).equals(MemberRank.MASTER)) {
return false;
}
}
}
try (final PreparedStatement ps = getDeleteCharacterData(con, characterId)) {
ps.executeUpdate();
}
try (final PreparedStatement ps = getDeleteHiredMerchants(con, characterId)) {
ps.executeUpdate();
}
try (final PreparedStatement ps = getDeleteMountData(con, characterId)) {
ps.executeUpdate();
}
try (final PreparedStatement ps = getDeleteMonsterBookData(con, characterId)) {
ps.executeUpdate();
}
return true;
} catch (final SQLException ex) {
System.err.println("Error while deleting character: " + ex);
}
return false;
}
private PreparedStatement getDeleteMonsterBookData(final Connection con, final int characterId) throws SQLException {
final PreparedStatement ps = con.prepareStatement("DELETE FROM monsterbook WHERE charid = ?");
ps.setInt(1, characterId);
return ps;
}
private PreparedStatement getDeleteMountData(final Connection con, final int characterId) throws SQLException {
final PreparedStatement ps = con.prepareStatement("DELETE FROM mountdata WHERE characterid = ?");
ps.setInt(1, characterId);
return ps;
}
private PreparedStatement getDeleteHiredMerchants(final Connection con, final int characterId) throws SQLException {
final PreparedStatement ps = con.prepareStatement("DELETE FROM hiredmerch WHERE characterid = ?");
ps.setInt(1, characterId);
return ps;
}
private PreparedStatement getDeleteCharacterData(final Connection con, final int characterId) throws SQLException {
final PreparedStatement ps = con.prepareStatement("DELETE FROM characters WHERE id = ?");
ps.setInt(1, characterId);
return ps;
}
private PreparedStatement getSelectGuildInfo(final Connection con, final int characterId) throws SQLException {
final PreparedStatement ps = con.prepareStatement("SELECT `id`, `guildid`, `guildrank` FROM `characters` WHERE `id` = ?");
ps.setInt(1, characterId);
return ps;
}
private Calendar getTempBanCalendar(final ResultSet rs) throws SQLException {
final Calendar lTempban = Calendar.getInstance();
if (rs.getLong("tempban") == 0) { // basically if timestamp in db is 0000-00-00
lTempban.setTimeInMillis(0);
return lTempban;
}
final Calendar today = Calendar.getInstance();
lTempban.setTimeInMillis(rs.getTimestamp("tempban").getTime());
if (today.getTimeInMillis() < lTempban.getTimeInMillis()) {
return lTempban;
}
lTempban.setTimeInMillis(0);
return lTempban;
}
public Calendar getTempBanCalendar() {
return this.temporaryBan;
}
public byte getTempBanReason() {
return this.temporaryBanReason;
}
public AuthReplyCode authenticate(final String username, final String inputPassword) {
final Connection connection = Database.getConnection();
try ( final PreparedStatement ps = this.getSelectAccount(connection, username);
final ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
final int banned = rs.getInt("banned");
if (banned > 0) {
return AuthReplyCode.DELETED_OR_BLOCKED;
} else if (banned == -1) {
this.unban();
}
super.setAccountId(rs.getInt("id"));
super.setAccountName(rs.getString("name"));
this.loginPassword = rs.getString("password");
this.loginPasswordSalt = rs.getString("salt");
this.characterPassword = rs.getString("char_password");
this.characterPasswordSalt = rs.getString("char_salt");
this.isGm = rs.getInt("gm") > 0;
this.temporaryBanReason = rs.getByte("tempban_reason");
this.temporaryBan = this.getTempBanCalendar(rs);
this.gender = Gender.fromNumber(rs.getByte("gender"));
this.characterSlots = rs.getInt("character_slots");
}
} catch (final SQLException e) {
System.err.println("ERROR" + e);
}
if (this.characterPassword != null && this.characterPasswordSalt != null) {
this.characterPassword = LoginCrypto.getPadding(this.characterPassword);
}
if (!LoginCrypto.checkSaltedSha512Hash(this.loginPassword, inputPassword, this.loginPasswordSalt)) {
return AuthReplyCode.WRONG_PASSWORD;
}
this.loggedIn = this.logOn();
if (this.loggedIn) {
return AuthReplyCode.SUCCESS;
} else {
return AuthReplyCode.ALREADY_LOGGED_IN;
}
}
private PreparedStatement getSelectAccount(final Connection connection, final String username) throws SQLException {
final String sql = "SELECT * FROM `accounts` WHERE `name` = ?";
final PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, username);
return ps;
}
private boolean logOn() {
boolean success = false;
final Connection connection = Database.getConnection();
try (final PreparedStatement ps = getUpdateLoggedInStatus(connection)) {
final int updatedRows = ps.executeUpdate();
success = updatedRows == 1;
} catch (final SQLException e) {
System.err.println("error updating login state" + e);
}
return success;
}
private PreparedStatement getUpdateLoggedInStatus(final Connection connection) throws SQLException {
final String sql = "UPDATE `accounts` SET `loggedin` = ?, `session_ip` = ?, `lastlogin` = CURRENT_TIMESTAMP() WHERE `id` = ? AND `loggedin` = ?";
final PreparedStatement ps = connection.prepareStatement(sql);
ps.setBoolean(1, true);
ps.setBoolean(4, false);
ps.setString(2, super.getSessionIP());
ps.setInt(3, this.getAccountId());
return ps;
}
public final Gender getGender() {
return this.gender;
}
public final void setGender(final Gender gender) {
this.gender = gender;
}
public final boolean isGm() {
return this.isGm;
}
private boolean checkCharacterPassword(final String password) {
boolean allow = false;
if (LoginCrypto.checkSaltedSha512Hash(this.characterPassword, password, this.characterPasswordSalt)) {
allow = true;
}
return allow;
}
@Override
public void disconnect(final boolean immediately) {
super.getSession().close(immediately);
}
private void allowNewCharacter(final int characterId) {
this.allowedChar.add(characterId);
}
private boolean isCharacterAuthorized(final int characterId) {
return this.allowedChar.contains(characterId);
}
private List<LoginCharacter> loadCharacters(final int worldId) {
// TODO make this less costly zZz
final List<LoginCharacter> list = LoginCharacter.loadCharacters(super.getAccountId(), worldId);
for (final LoginCharacter character : list) {
this.allowedChar.add(character.getId());
}
return list;
}
public final String getCharacterPassword() {
return this.characterPassword;
}
private boolean canAttemptAgain() {
return this.loginAttempt++ <= LOGIN_ATTEMPTS;
}
public void handleLogin(final PacketReader reader) throws PacketFormatException {
final String login = reader.readLengthPrefixedString();
final String password = LoginCrypto.decryptRSA(reader.readLengthPrefixedString());
final boolean ipBan = this.hasBannedIP();
AuthReplyCode code = this.authenticate(login, password);
final Calendar tempbannedTill = this.getTempBanCalendar();
if (code == AuthReplyCode.SUCCESS && ipBan) {
code = AuthReplyCode.DELETED_OR_BLOCKED;
}
if (code != AuthReplyCode.SUCCESS) {
if (this.canAttemptAgain()) {
this.write(LoginPacket.getLoginFailed(code));
}
} else if (tempbannedTill.getTimeInMillis() != 0) {
if (this.canAttemptAgain()) {
this.write(LoginPacket.getTempBan(FiletimeUtil.getFiletime(tempbannedTill.getTimeInMillis()), this.getTempBanReason()));
}
} else {
this.loginAttempt = 0;
LoginWorker.registerClient(this);
}
}
public void handleWithoutSecondPassword(final PacketReader reader) throws PacketFormatException {
reader.skip(1);
final int characterId = reader.readInt();
if (!this.isCharacterAuthorized(characterId)) {
this.disconnect(true);
return;
}
final String password = this.characterPassword;
if (reader.remaining() > 0) {
if (password != null) {
// Hack
this.disconnect(true);
return;
}
final String input = reader.readLengthPrefixedString();
if (input.length() >= 4 && input.length() <= 16) {
this.updateCharacterPassword(input);
} else {
this.write(LoginPacket.characterPasswordError((byte) 0x14));
return;
}
} else if (!this.canAttemptAgain() || password != null) {
this.disconnect();
return;
}
// TODO: transfer() method
if (this.getIdleTask() != null) {
this.getIdleTask().cancel(true);
}
final int port = LoginServer.getInstance().getChannelServerPort(this.getChannelId());
this.write(CommonPackets.getServerIP(port, characterId));
}
private void updateCharacterPassword(final String newPassword) {
final Connection connection = Database.getConnection();
final String newSalt = LoginCrypto.makeSalt();
final String newHash = LoginCrypto.padWithRandom(LoginCrypto.makeSaltedSha512Hash(newPassword, newSalt));
try (PreparedStatement ps = getUpdatePassword(connection, newHash, newSalt)) {
ps.executeUpdate();
this.characterPassword = newPassword;
this.characterPasswordSalt = newSalt;
} catch (final SQLException e) {
System.err.println("Error updating character password: " + e);
}
}
private PreparedStatement getUpdatePassword(final Connection connection, final String newHash, final String newSalt) throws SQLException {
final PreparedStatement ps = connection.prepareStatement("UPDATE `accounts` SET `char_password` = ?, `char_salt` = ? WHERE `id` = ?");
ps.setString(1, newHash);
ps.setString(2, newSalt);
ps.setInt(3, super.getAccountId());
return ps;
}
public void handleWithSecondPassword(final PacketReader reader) throws PacketFormatException {
final String password = reader.readLengthPrefixedString();
final int characterId = reader.readInt();
if (!this.canAttemptAgain() || this.getCharacterPassword() == null || !this.isCharacterAuthorized(characterId)) {
this.disconnect();
return;
}
if (this.checkCharacterPassword(password)) {
// TODO: transfer() method
if (this.getIdleTask() != null) {
this.getIdleTask().cancel(true);
}
final int port = LoginServer.getInstance().getChannelServerPort(this.getChannelId());
this.write(CommonPackets.getServerIP(port, characterId));
} else {
this.write(LoginPacket.characterPasswordError((byte) 0x14));
}
}
public void handleWorldListRequest() {
final LoginServer ls = LoginServer.getInstance();
this.write(LoginPacket.getWorldList(2, "Cassiopeia", ls.getChannels()));
this.write(LoginPacket.getEndOfWorldList());
}
public void handleServerStatusRequest() {
// TODO: Get number of connected clients from chosen channel.
// (this packet is for the channel, not the world, nib who wrote this.
this.write(LoginPacket.getWorldStatus(0));
}
public void handleCharacterListRequest(final PacketReader reader) throws PacketFormatException {
final int server = reader.readByte();
final int channel = reader.readByte() + 1;
this.setWorldId(server);
System.out.println(":: Client is connecting to server " + server + " channel " + channel + " ::");
this.setChannelId(channel);
final List<LoginCharacter> chars = this.loadCharacters(server);
if (chars != null) {
this.write(LoginPacket.getCharacterList(this.getCharacterPassword() != null, chars, this.characterSlots));
} else {
this.disconnect();
}
}
public void handleCharacterNameCheck(final PacketReader reader) throws PacketFormatException {
final String name = reader.readLengthPrefixedString();
final boolean isValid = GameCharacterUtil.validateCharacterName(name);
final boolean isAvailable = GameCharacterUtil.isAvailableName(name);
final boolean isAllowed = LoginInfoProvider.getInstance().isAllowedName(name);
final boolean conditions = isValid && isAvailable && isAllowed;
this.write(LoginPacket.charNameResponse(name, conditions));
}
public void handleCreateCharacter(final PacketReader reader) throws PacketFormatException {
final String name = reader.readLengthPrefixedString();
final int jobCategory = reader.readInt();
final short db = reader.readShort();
final int faceId = reader.readInt();
final int hairId = reader.readInt();
final int hairColor = reader.readInt();
final int skinColorId = reader.readInt();
final int top = reader.readInt();
final int bottom = reader.readInt();
final int shoes = reader.readInt();
final int weapon = reader.readInt();
final LoginCharacter newchar = LoginCharacter.getDefault(jobCategory);
newchar.setName(name);
newchar.setWorldId(this.getWorldId());
newchar.setFaceId(faceId);
newchar.setHairId(hairId + hairColor);
newchar.setGender(this.gender);
newchar.setSkinColorId(skinColorId);
final Inventory equip = newchar.getEquippedItemsInventory();
final LoginInfoProvider li = LoginInfoProvider.getInstance();
Item item = li.getEquipById(top);
item.setPosition((byte) -5);
equip.addFromDb(item);
item = li.getEquipById(bottom);
item.setPosition((byte) -6);
equip.addFromDb(item);
item = li.getEquipById(shoes);
item.setPosition((byte) -7);
equip.addFromDb(item);
item = li.getEquipById(weapon);
item.setPosition((byte) -11);
equip.addFromDb(item);
final boolean isValid = GameCharacterUtil.validateCharacterName(name);
final boolean isAvailable = GameCharacterUtil.isAvailableName(name);
final boolean isAllowed = li.isAllowedName(name);
if (isValid && isAvailable && isAllowed) {
final boolean isDualBlader = jobCategory == 1 && db > 0;
LoginCharacter.saveNewCharacterToDb(newchar, jobCategory, isDualBlader);
this.allowNewCharacter(newchar.getId());
this.write(LoginPacket.addNewCharEntry(newchar, true));
} else {
this.write(LoginPacket.addNewCharEntry(newchar, false));
}
}
public void handleDeleteCharacter(final PacketReader reader) throws PacketFormatException {
String password = null;
final byte withPassword = reader.readByte();
if (withPassword > 0) {
password = reader.readLengthPrefixedString();
}
// read passport string
reader.readLengthPrefixedString();
final int characterId = reader.readInt();
if (!this.isCharacterAuthorized(characterId)) {
this.disconnect(true);
return;
}
byte state = 0;
if (this.characterPassword != null) {
if (password == null) {
this.disconnect(true);
return;
} else {
if (!this.checkCharacterPassword(password)) {
state = 12;
}
}
}
if (state == 0) {
if (!this.deleteCharacter(characterId)) {
state = 1;
}
}
this.write(LoginPacket.deleteCharResponse(characterId, state));
}
}