/*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If
* not, see <http://www.gnu.org/licenses/>.
*/
package silentium.gameserver;
import javolution.util.FastList;
import javolution.util.FastMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import silentium.commons.crypt.NewCrypt;
import silentium.commons.utils.Rnd;
import silentium.commons.utils.Util;
import silentium.gameserver.configs.HexidConfig;
import silentium.gameserver.configs.MainConfig;
import silentium.gameserver.model.L2World;
import silentium.gameserver.model.actor.instance.L2PcInstance;
import silentium.gameserver.network.L2GameClient;
import silentium.gameserver.network.L2GameClient.GameClientState;
import silentium.gameserver.network.gameserverpackets.*;
import silentium.gameserver.network.loginserverpackets.*;
import silentium.gameserver.network.serverpackets.AuthLoginFail;
import silentium.gameserver.network.serverpackets.CharSelectInfo;
import java.io.*;
import java.math.BigInteger;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAKeyGenParameterSpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public class LoginServerThread extends Thread {
protected static final Logger _log = LoggerFactory.getLogger(LoginServerThread.class.getName());
private static final int REVISION = 0x0102;
private RSAPublicKey _publicKey;
private final String _hostname;
private final int _port;
private final int _gamePort;
private Socket _loginSocket;
private InputStream _in;
private OutputStream _out;
/**
* The BlowFish engine used to encrypt packets<br>
* It is first initialized with a unified key:<br>
* "_;v.]05-31!|+-%xT!^[$\00"<br>
* <br>
* and then after handshake, with a new key sent by<br>
* loginserver during the handshake. This new key is stored<br>
* in {@link #_blowfishKey}
*/
private NewCrypt _blowfish;
private byte[] _blowfishKey;
private byte[] _hexID;
private final boolean _acceptAlternate;
private int _requestID;
private int _serverID;
private final boolean _reserveHost;
private int _maxPlayer;
private final List<WaitingClient> _waitingClients;
private final Map<String, L2GameClient> _accountsInGameServer;
private int _status;
private String _serverName;
private final String _gameExternalHost;
private final String _gameInternalHost;
protected LoginServerThread() {
super("LoginServerThread");
_port = MainConfig.GAME_SERVER_LOGIN_PORT;
_gamePort = MainConfig.PORT_GAME;
_hostname = MainConfig.GAME_SERVER_LOGIN_HOST;
_hexID = HexidConfig.HEX_ID;
if (_hexID == null) {
_requestID = MainConfig.REQUEST_ID;
_hexID = generateHex(16);
} else {
_requestID = HexidConfig.SERVER_ID;
}
_acceptAlternate = MainConfig.ACCEPT_ALTERNATE_ID;
_reserveHost = MainConfig.RESERVE_HOST_ON_LOGIN;
_gameExternalHost = MainConfig.EXTERNAL_HOSTNAME;
_gameInternalHost = MainConfig.INTERNAL_HOSTNAME;
_waitingClients = new FastList<>();
_accountsInGameServer = new FastMap<String, L2GameClient>().shared();
_maxPlayer = MainConfig.MAXIMUM_ONLINE_USERS;
}
public static LoginServerThread getInstance() {
return SingletonHolder._instance;
}
@Override
public void run() {
while (!isInterrupted()) {
int lengthHi = 0;
int lengthLo = 0;
int length = 0;
boolean checksumOk = false;
try {
// Connection
_log.info("Connecting to login on " + _hostname + ":" + _port);
_loginSocket = new Socket(_hostname, _port);
_in = new BufferedInputStream(_loginSocket.getInputStream());
_out = new BufferedOutputStream(_loginSocket.getOutputStream());
// init Blowfish
_blowfishKey = generateHex(40);
_blowfish = new NewCrypt("_;v.]05-31!|+-%xT!^[$\00");
while (!isInterrupted()) {
lengthLo = _in.read();
lengthHi = _in.read();
length = lengthHi * 256 + lengthLo;
if (lengthHi < 0) {
_log.trace("LoginServerThread: Login terminated the connection.");
break;
}
byte[] incoming = new byte[length - 2];
int receivedBytes = 0;
int newBytes = 0;
int left = length - 2;
while (newBytes != -1 && receivedBytes < length - 2) {
newBytes = _in.read(incoming, receivedBytes, left);
receivedBytes = receivedBytes + newBytes;
left -= newBytes;
}
if (receivedBytes != length - 2) {
_log.warn("Incomplete Packet is sent to the server, closing connection.(LS)");
break;
}
// decrypt if we have a key
byte[] decrypt = _blowfish.decrypt(incoming);
checksumOk = NewCrypt.verifyChecksum(decrypt);
if (!checksumOk) {
_log.warn("Incorrect packet checksum, ignoring packet (LS)");
break;
}
_log.debug("[C]\n" + Util.printData(decrypt));
int packetType = decrypt[0] & 0xff;
switch (packetType) {
case 0x00:
InitLS init = new InitLS(decrypt);
_log.debug("Init received");
if (init.getRevision() != REVISION) {
_log.warn("/!\\ Revision mismatch between LS and GS /!\\");
break;
}
try {
KeyFactory kfac = KeyFactory.getInstance("RSA");
BigInteger modulus = new BigInteger(init.getRSAKey());
RSAPublicKeySpec kspec1 = new RSAPublicKeySpec(modulus, RSAKeyGenParameterSpec.F4);
_publicKey = (RSAPublicKey) kfac.generatePublic(kspec1);
_log.debug("RSA key set up");
} catch (GeneralSecurityException e) {
_log.warn("Troubles while init the public key send by login");
break;
}
// send the blowfish key through the rsa encryption
BlowFishKey bfk = new BlowFishKey(_blowfishKey, _publicKey);
sendPacket(bfk);
_log.debug("Sent new blowfish key");
// now, only accept paket with the new encryption
_blowfish = new NewCrypt(_blowfishKey);
_log.debug("Changed blowfish key");
AuthRequest ar = new AuthRequest(_requestID, _acceptAlternate, _hexID, _gameExternalHost, _gameInternalHost, _gamePort, _reserveHost, _maxPlayer);
sendPacket(ar);
_log.debug("Sent AuthRequest to login");
break;
case 0x01:
LoginServerFail lsf = new LoginServerFail(decrypt);
_log.info("Damn! Registeration Failed: " + lsf.getReasonString());
// login will close the connection here
break;
case 0x02:
AuthResponse aresp = new AuthResponse(decrypt);
_serverID = aresp.getServerId();
_serverName = aresp.getServerName();
HexidConfig.saveHexid(_serverID, hexToString(_hexID));
_log.info("Registered on login as server: [" + _serverID + "] " + _serverName);
ServerStatus st = new ServerStatus();
if (MainConfig.SERVER_LIST_BRACKET)
st.addAttribute(ServerStatus.SERVER_LIST_SQUARE_BRACKET, ServerStatus.ON);
else
st.addAttribute(ServerStatus.SERVER_LIST_SQUARE_BRACKET, ServerStatus.OFF);
if (MainConfig.SERVER_LIST_CLOCK)
st.addAttribute(ServerStatus.SERVER_LIST_CLOCK, ServerStatus.ON);
else
st.addAttribute(ServerStatus.SERVER_LIST_CLOCK, ServerStatus.OFF);
if (MainConfig.SERVER_LIST_TESTSERVER)
st.addAttribute(ServerStatus.TEST_SERVER, ServerStatus.ON);
else
st.addAttribute(ServerStatus.TEST_SERVER, ServerStatus.OFF);
if (MainConfig.SERVER_GMONLY)
st.addAttribute(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_GM_ONLY);
else
st.addAttribute(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_AUTO);
sendPacket(st);
if (L2World.getInstance().getAllPlayersCount() > 0) {
FastList<String> playerList = new FastList<>();
Collection<L2PcInstance> pls = L2World.getInstance().getAllPlayers().values();
for (L2PcInstance player : pls)
playerList.add(player.getAccountName());
PlayerInGame pig = new PlayerInGame(playerList);
sendPacket(pig);
}
break;
case 0x03:
PlayerAuthResponse par = new PlayerAuthResponse(decrypt);
String account = par.getAccount();
WaitingClient wcToRemove = null;
synchronized (_waitingClients) {
for (WaitingClient wc : _waitingClients) {
if (wc.account.equals(account))
wcToRemove = wc;
}
}
if (wcToRemove != null) {
if (par.isAuthed()) {
_log.debug("Login accepted player " + wcToRemove.account + " waited(" + (GameTimeController.getGameTicks() - wcToRemove.timestamp) + "ms)");
PlayerInGame pig = new PlayerInGame(par.getAccount());
sendPacket(pig);
wcToRemove.gameClient.setState(GameClientState.AUTHED);
wcToRemove.gameClient.setSessionId(wcToRemove.session);
CharSelectInfo cl = new CharSelectInfo(wcToRemove.account, wcToRemove.gameClient.getSessionId().playOkID1);
wcToRemove.gameClient.getConnection().sendPacket(cl);
wcToRemove.gameClient.setCharSelection(cl.getCharInfo());
} else {
_log.warn("Session key is not correct. closing connection");
wcToRemove.gameClient.getConnection().sendPacket(new AuthLoginFail(1));
wcToRemove.gameClient.closeNow();
}
_waitingClients.remove(wcToRemove);
}
break;
case 0x04:
KickPlayer kp = new KickPlayer(decrypt);
doKickPlayer(kp.getAccount());
break;
}
}
} catch (UnknownHostException e) {
_log.debug(e.getLocalizedMessage(), e);
} catch (IOException e) {
_log.info("No connection found with loginserver, next try in 10 seconds.");
} finally {
try {
_loginSocket.close();
if (isInterrupted())
return;
} catch (Exception e) {
}
}
// 10 seconds tempo before another try
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
}
}
public void addWaitingClientAndSendRequest(String acc, L2GameClient client, SessionKey key) {
_log.debug(String.valueOf(key));
WaitingClient wc = new WaitingClient(acc, client, key);
synchronized (_waitingClients) {
_waitingClients.add(wc);
}
PlayerAuthRequest par = new PlayerAuthRequest(acc, key);
try {
sendPacket(par);
} catch (IOException e) {
_log.warn("Error while sending player auth request.", e);
}
}
public void removeWaitingClient(L2GameClient client) {
WaitingClient toRemove = null;
synchronized (_waitingClients) {
for (WaitingClient c : _waitingClients) {
if (c.gameClient == client)
toRemove = c;
}
if (toRemove != null)
_waitingClients.remove(toRemove);
}
}
public void sendLogout(String account) {
if (account == null)
return;
PlayerLogout pl = new PlayerLogout(account);
try {
sendPacket(pl);
} catch (IOException e) {
_log.warn("Error while sending logout packet to login.", e);
} finally {
_accountsInGameServer.remove(account);
}
}
public void addGameServerLogin(String account, L2GameClient client) {
_accountsInGameServer.put(account, client);
}
public void sendAccessLevel(String account, int level) {
ChangeAccessLevel cal = new ChangeAccessLevel(account, level);
try {
sendPacket(cal);
} catch (IOException e) {
_log.warn(e.getLocalizedMessage(), e);
}
}
private static String hexToString(byte[] hex) {
return new BigInteger(hex).toString(16);
}
public void doKickPlayer(String account) {
if (_accountsInGameServer.get(account) != null) {
_accountsInGameServer.get(account).closeNow();
LoginServerThread.getInstance().sendLogout(account);
}
}
public static byte[] generateHex(int size) {
byte[] array = new byte[size];
Rnd.nextBytes(array);
_log.trace("Generated random String: \"" + array + "\"");
return array;
}
private void sendPacket(GameServerBasePacket sl) throws IOException {
byte[] data = sl.getContent();
NewCrypt.appendChecksum(data);
_log.trace("[S]\n" + Util.printData(data));
data = _blowfish.crypt(data);
int len = data.length + 2;
synchronized (_out) // avoids tow threads writing in the mean time
{
_out.write(len & 0xff);
_out.write(len >> 8 & 0xff);
_out.write(data);
_out.flush();
}
}
public void setMaxPlayer(int maxPlayer) {
sendServerStatus(ServerStatus.MAX_PLAYERS, maxPlayer);
_maxPlayer = maxPlayer;
}
public int getMaxPlayer() {
return _maxPlayer;
}
public void sendServerStatus(int id, int value) {
ServerStatus ss = new ServerStatus();
ss.addAttribute(id, value);
try {
sendPacket(ss);
} catch (IOException e) {
_log.warn(e.getLocalizedMessage(), e);
}
}
public String getStatusString() {
return ServerStatus.STATUS_STRING[_status];
}
public boolean isClockShown() {
return MainConfig.SERVER_LIST_CLOCK;
}
public boolean isBracketShown() {
return MainConfig.SERVER_LIST_BRACKET;
}
public String getServerName() {
return _serverName;
}
public void setServerStatus(int status) {
switch (status) {
case ServerStatus.STATUS_AUTO:
sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_AUTO);
_status = status;
break;
case ServerStatus.STATUS_DOWN:
sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_DOWN);
_status = status;
break;
case ServerStatus.STATUS_FULL:
sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_FULL);
_status = status;
break;
case ServerStatus.STATUS_GM_ONLY:
sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_GM_ONLY);
_status = status;
break;
case ServerStatus.STATUS_GOOD:
sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_GOOD);
_status = status;
break;
case ServerStatus.STATUS_NORMAL:
sendServerStatus(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_NORMAL);
_status = status;
break;
default:
throw new IllegalArgumentException("Status does not exists:" + status);
}
}
public static class SessionKey {
public int playOkID1;
public int playOkID2;
public int loginOkID1;
public int loginOkID2;
public SessionKey(int loginOK1, int loginOK2, int playOK1, int playOK2) {
playOkID1 = playOK1;
playOkID2 = playOK2;
loginOkID1 = loginOK1;
loginOkID2 = loginOK2;
}
@Override
public String toString() {
return "PlayOk: " + playOkID1 + " " + playOkID2 + " LoginOk:" + loginOkID1 + " " + loginOkID2;
}
}
private class WaitingClient {
public int timestamp;
public String account;
public L2GameClient gameClient;
public SessionKey session;
public WaitingClient(String acc, L2GameClient client, SessionKey key) {
account = acc;
timestamp = GameTimeController.getGameTicks();
gameClient = client;
session = key;
}
}
private static class SingletonHolder {
protected static final LoginServerThread _instance = new LoginServerThread();
}
}