|
|
|
// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
|
|
|
|
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
|
|
|
|
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
|
|
|
|
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
|
|
|
|
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
|
|
|
|
// Magicbane Emulator Project © 2013 - 2022
|
|
|
|
// www.magicbane.com
|
|
|
|
|
|
|
|
|
|
|
|
package engine.net.client;
|
|
|
|
|
|
|
|
import engine.Enum;
|
|
|
|
import engine.gameManager.ConfigManager;
|
|
|
|
import engine.gameManager.SessionManager;
|
|
|
|
import engine.job.JobScheduler;
|
|
|
|
import engine.jobs.DisconnectJob;
|
|
|
|
import engine.net.AbstractConnection;
|
|
|
|
import engine.net.AbstractNetMsg;
|
|
|
|
import engine.net.Network;
|
|
|
|
import engine.net.client.msg.ClientNetMsg;
|
|
|
|
import engine.net.client.msg.login.LoginErrorMsg;
|
|
|
|
import engine.objects.Account;
|
|
|
|
import engine.objects.PlayerCharacter;
|
|
|
|
import engine.server.MBServerStatics;
|
|
|
|
import engine.session.SessionID;
|
|
|
|
import org.pmw.tinylog.Logger;
|
|
|
|
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.nio.ByteBuffer;
|
|
|
|
import java.nio.channels.ClosedChannelException;
|
|
|
|
import java.nio.channels.NotYetConnectedException;
|
|
|
|
import java.nio.channels.SocketChannel;
|
|
|
|
import java.util.concurrent.locks.ReentrantLock;
|
|
|
|
|
|
|
|
public class ClientConnection extends AbstractConnection {
|
|
|
|
|
|
|
|
private final ClientAuthenticator crypto;
|
|
|
|
private final String clientIpAddress;
|
|
|
|
public String machineID;
|
|
|
|
public long guildtreespam = 0;
|
|
|
|
public long ordernpcspam = 0;
|
|
|
|
public ReentrantLock trainLock = new ReentrantLock();
|
|
|
|
public ReentrantLock sellLock = new ReentrantLock();
|
|
|
|
public ReentrantLock buyLock = new ReentrantLock();
|
|
|
|
public boolean desyncDebug = false;
|
|
|
|
public byte[] lastByteBuffer;
|
|
|
|
protected SessionID sessionID = null;
|
|
|
|
private byte cryptoInitTries = 0;
|
|
|
|
|
|
|
|
public ClientConnection(ClientConnectionManager connMan,
|
|
|
|
SocketChannel sockChan) {
|
|
|
|
super(connMan, sockChan, true);
|
|
|
|
this.crypto = new ClientAuthenticator(this);
|
|
|
|
|
|
|
|
this.clientIpAddress = sockChan.socket().getRemoteSocketAddress()
|
|
|
|
.toString().replace("/", "").split(":")[0];
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected boolean _sendMsg(AbstractNetMsg msg) {
|
|
|
|
try {
|
|
|
|
msg.setOrigin(this);
|
|
|
|
ByteBuffer bb = msg.serialize();
|
|
|
|
|
|
|
|
// Application protocol logging toggled via
|
|
|
|
// DevCmd: netdebug on|off
|
|
|
|
|
|
|
|
if (MBServerStatics.DEBUG_PROTOCOL)
|
|
|
|
applicationProtocolLogger(msg, MessageSource.SOURCE_SERVER);
|
|
|
|
|
|
|
|
boolean retval = this.sendBB(bb);
|
|
|
|
Network.byteBufferPool.putBuffer(bb);//return here.
|
|
|
|
|
|
|
|
return retval;
|
|
|
|
|
|
|
|
} catch (Exception e) { // Catch-all
|
|
|
|
Logger.error(e);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sending a NetMsg to the client involves NOT including a dataLen parameter
|
|
|
|
* and also involves encrypting the data.
|
|
|
|
*/
|
|
|
|
@Override
|
|
|
|
protected boolean _sendBB(ByteBuffer bb) {
|
|
|
|
boolean useCrypto = this.crypto.initialized();
|
|
|
|
boolean retVal;
|
|
|
|
|
|
|
|
// Logger.debug("useCrypto: " + useCrypto + ". bb.cap(): " +
|
|
|
|
// bb.capacity());
|
|
|
|
if (useCrypto == false)
|
|
|
|
retVal = super._sendBB(bb);
|
|
|
|
else {
|
|
|
|
if (bb == null)
|
|
|
|
Logger.error("Incoming bb is null");
|
|
|
|
ByteBuffer encrypted = Network.byteBufferPool.getBufferToFit(bb.capacity());
|
|
|
|
if (encrypted == null)
|
|
|
|
Logger.error("Encrypted bb is null");
|
|
|
|
this.crypto.encrypt(bb, encrypted);
|
|
|
|
retVal = super._sendBB(encrypted);
|
|
|
|
}
|
|
|
|
|
|
|
|
return retVal;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Receiving data from a client involves the initial Crypto Key Exchange,
|
|
|
|
* waiting for a complete NetMsg to arrive using an accumulation factory and
|
|
|
|
* decrypting the data.
|
|
|
|
*/
|
|
|
|
//FIXME the int return value on this means nothing! Clean it up!
|
|
|
|
@Override
|
|
|
|
protected int read() {
|
|
|
|
|
|
|
|
if (readLock.tryLock())
|
|
|
|
try {
|
|
|
|
|
|
|
|
// First and foremost, check to see if we the Crypto is initted yet
|
|
|
|
if (!this.crypto.initialized())
|
|
|
|
this.crypto.initialize(this);
|
|
|
|
|
|
|
|
if (!this.crypto.initialized()) {
|
|
|
|
++this.cryptoInitTries;
|
|
|
|
if (this.cryptoInitTries >= MBServerStatics.MAX_CRYPTO_INIT_TRIES) {
|
|
|
|
Logger.info("Failed to initialize after "
|
|
|
|
+ MBServerStatics.MAX_CRYPTO_INIT_TRIES
|
|
|
|
+ " tries. Disconnecting.");
|
|
|
|
this.disconnect();
|
|
|
|
}
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// check to see if SessionID == null;
|
|
|
|
if (this.sessionID == null)
|
|
|
|
this.sessionID = new SessionID(this.crypto.getSecretKeyBytes());
|
|
|
|
|
|
|
|
// Get ByteBuffers out of pool.
|
|
|
|
ByteBuffer bb = Network.byteBufferPool.getBuffer(16);
|
|
|
|
ByteBuffer decrypted = Network.byteBufferPool.getBuffer(16);
|
|
|
|
// ByteBuffer bb = ByteBuffer.allocate(1024 * 4);
|
|
|
|
|
|
|
|
int totalBytesRead = 0;
|
|
|
|
int lastRead = 0;
|
|
|
|
do {
|
|
|
|
try {
|
|
|
|
bb.clear();
|
|
|
|
decrypted.clear();
|
|
|
|
lastRead = this.sockChan.read(bb);
|
|
|
|
// On EOF on the SocketChannel, disconnect.
|
|
|
|
if (lastRead <= -1) {
|
|
|
|
this.disconnect();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (lastRead == 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
// ByteBuffer decrypted = ByteBuffer.allocate(lastRead);
|
|
|
|
this.crypto.decrypt(bb, decrypted);
|
|
|
|
this.factory.addData(decrypted);
|
|
|
|
|
|
|
|
|
|
|
|
this.checkInternalFactory();
|
|
|
|
|
|
|
|
totalBytesRead += lastRead;
|
|
|
|
|
|
|
|
|
|
|
|
} catch (NotYetConnectedException e) {
|
|
|
|
Logger.error(e.getLocalizedMessage());
|
|
|
|
totalBytesRead = -1; // Set error retVal
|
|
|
|
break;
|
|
|
|
|
|
|
|
} catch (ClosedChannelException e) {
|
|
|
|
// TODO Should a closed channel be logged or just cleaned up?
|
|
|
|
// this.logEXCEPTION(e);
|
|
|
|
this.disconnect();
|
|
|
|
totalBytesRead = -1; // Set error retVal
|
|
|
|
break;
|
|
|
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
if (e.getLocalizedMessage() != null && (!e.getLocalizedMessage().equals(MBServerStatics.EXISTING_CONNECTION_CLOSED) && !e.getLocalizedMessage().equals(MBServerStatics.RESET_BY_PEER))) {
|
|
|
|
Logger.info("Error Reading message opcode " + this.lastOpcode);
|
|
|
|
Logger.error(e);
|
|
|
|
}
|
|
|
|
this.disconnect();
|
|
|
|
totalBytesRead = -1; // Set error retVal
|
|
|
|
break;
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
|
|
Logger.info("Error Reading message opcode " + this.lastOpcode);
|
|
|
|
Logger.error(e);
|
|
|
|
totalBytesRead = -1; // Set error retVal
|
|
|
|
this.disconnect();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
while (lastRead > 0);
|
|
|
|
|
|
|
|
Network.byteBufferPool.putBuffer(bb);
|
|
|
|
Network.byteBufferPool.putBuffer(decrypted);
|
|
|
|
|
|
|
|
return totalBytesRead;
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
readLock.unlock();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
Logger.debug("Another thread already has a read lock! Skipping.");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void disconnect() {
|
|
|
|
super.disconnect();
|
|
|
|
try {
|
|
|
|
|
|
|
|
if (ConfigManager.serverType.equals(Enum.ServerType.WORLDSERVER))
|
|
|
|
ConfigManager.worldServer.removeClient(this);
|
|
|
|
else
|
|
|
|
ConfigManager.loginServer.removeClient(this);
|
|
|
|
|
|
|
|
// TODO There has to be a more direct way to do this...
|
|
|
|
SessionManager.remSession(
|
|
|
|
SessionManager.getSession(sessionID));
|
|
|
|
} catch (NullPointerException e) {
|
|
|
|
Logger
|
|
|
|
.error(
|
|
|
|
"Tried to remove improperly initialized session. Skipping." +
|
|
|
|
e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public void forceDisconnect() {
|
|
|
|
super.disconnect();
|
|
|
|
}
|
|
|
|
|
|
|
|
public SessionID getSessionID() {
|
|
|
|
return sessionID;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Getters n setters
|
|
|
|
*/
|
|
|
|
|
|
|
|
public byte[] getSecretKeyBytes() {
|
|
|
|
return this.crypto.getSecretKeyBytes();
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Convenience getters for SessionManager
|
|
|
|
*/
|
|
|
|
public Account getAccount() {
|
|
|
|
return SessionManager.getAccount(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
public PlayerCharacter getPlayerCharacter() {
|
|
|
|
return SessionManager.getPlayerCharacter(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean handleClientMsg(ClientNetMsg msg) {
|
|
|
|
|
|
|
|
Protocol protocolMsg = msg.getProtocolMsg();
|
|
|
|
|
|
|
|
switch (protocolMsg) {
|
|
|
|
case KEEPALIVESERVERCLIENT:
|
|
|
|
this.setLastKeepAliveTime();
|
|
|
|
break;
|
|
|
|
// case ClientOpcodes.OpenVault:
|
|
|
|
// case ClientOpcodes.Random:
|
|
|
|
// case ClientOpcodes.DoorTryOpen:
|
|
|
|
// case ClientOpcodes.SetSelectedObect:
|
|
|
|
// case ClientOpcodes.MoveObjectToContainer:
|
|
|
|
// case ClientOpcodes.ToggleSitStand:
|
|
|
|
// case ClientOpcodes.SocialChannel:
|
|
|
|
// case ClientOpcodes.OpenFriendsCondemnList:
|
|
|
|
case SELLOBJECT:
|
|
|
|
this.setLastMsgTime();
|
|
|
|
break;
|
|
|
|
case MOVETOPOINT:
|
|
|
|
case ARCCOMBATMODEATTACKING:
|
|
|
|
this.setLastMsgTime();
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
this.setLastMsgTime();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Application protocol logging toggled via
|
|
|
|
// DevCmd: netdebug on|off
|
|
|
|
|
|
|
|
if (MBServerStatics.DEBUG_PROTOCOL)
|
|
|
|
applicationProtocolLogger(msg, MessageSource.SOURCE_CLIENT);
|
|
|
|
|
|
|
|
return ConfigManager.handler.handleClientMsg(msg); // *** Refactor : Null check then call
|
|
|
|
}
|
|
|
|
|
|
|
|
private void applicationProtocolLogger(AbstractNetMsg msg, MessageSource origin) {
|
|
|
|
|
|
|
|
String outString = "";
|
|
|
|
PlayerCharacter tempPlayer = null;
|
|
|
|
|
|
|
|
// Log the protocolMsg
|
|
|
|
if (origin == MessageSource.SOURCE_CLIENT)
|
|
|
|
outString = " Incoming protocolMsg: ";
|
|
|
|
else
|
|
|
|
outString = " Outgoing protocolMsg: ";
|
|
|
|
|
|
|
|
Logger.info(outString
|
|
|
|
+ Integer.toHexString(msg.getProtocolMsg().opcode)
|
|
|
|
+ '/' + msg.getProtocolMsg());
|
|
|
|
|
|
|
|
// Dump message contents using reflection
|
|
|
|
tempPlayer = this.getPlayerCharacter();
|
|
|
|
outString = "";
|
|
|
|
outString += (tempPlayer == null) ? "PlayerUnknown" : tempPlayer.getFirstName() + ' '
|
|
|
|
+ msg.toString();
|
|
|
|
Logger.info(outString);
|
|
|
|
}
|
|
|
|
// Method logs detailed information about application
|
|
|
|
// protocol traffic. Toggled at runtime via the
|
|
|
|
// DevCmd netdebug on|off
|
|
|
|
|
|
|
|
public void kickToLogin(int errCode, String message) {
|
|
|
|
|
|
|
|
LoginErrorMsg lom = new LoginErrorMsg(errCode, message);
|
|
|
|
|
|
|
|
if (!sendMsg(lom))
|
|
|
|
Logger.error("Failed to send message"); // TODO Do we just accept this failure to send Msg?
|
|
|
|
|
|
|
|
DisconnectJob dj = new DisconnectJob(this);
|
|
|
|
JobScheduler.getInstance().scheduleJob(dj, 250);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
public final String getClientIpAddress() {
|
|
|
|
return this.clientIpAddress;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Enumeration of a message's origin for logging purposes
|
|
|
|
private enum MessageSource {
|
|
|
|
|
|
|
|
SOURCE_CLIENT,
|
|
|
|
SOURCE_SERVER
|
|
|
|
}
|
|
|
|
}
|