/*
* Copyright (C) 2004-2015 L2J Server
*
* This file is part of L2J Server.
*
* L2J Server 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.
*
* L2J Server 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 .
*/
package com.l2jserver.gameserver;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.Socket;
import java.net.SocketException;
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.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.l2jserver.Config;
import com.l2jserver.commons.database.pool.impl.ConnectionFactory;
import com.l2jserver.gameserver.model.L2World;
import com.l2jserver.gameserver.model.actor.instance.L2PcInstance;
import com.l2jserver.gameserver.network.L2GameClient;
import com.l2jserver.gameserver.network.L2GameClient.GameClientState;
import com.l2jserver.gameserver.network.SystemMessageId;
import com.l2jserver.gameserver.network.gameserverpackets.AuthRequest;
import com.l2jserver.gameserver.network.gameserverpackets.BlowFishKey;
import com.l2jserver.gameserver.network.gameserverpackets.ChangeAccessLevel;
import com.l2jserver.gameserver.network.gameserverpackets.ChangePassword;
import com.l2jserver.gameserver.network.gameserverpackets.PlayerAuthRequest;
import com.l2jserver.gameserver.network.gameserverpackets.PlayerInGame;
import com.l2jserver.gameserver.network.gameserverpackets.PlayerLogout;
import com.l2jserver.gameserver.network.gameserverpackets.PlayerTracert;
import com.l2jserver.gameserver.network.gameserverpackets.ReplyCharacters;
import com.l2jserver.gameserver.network.gameserverpackets.SendMail;
import com.l2jserver.gameserver.network.gameserverpackets.ServerStatus;
import com.l2jserver.gameserver.network.gameserverpackets.TempBan;
import com.l2jserver.gameserver.network.loginserverpackets.AuthResponse;
import com.l2jserver.gameserver.network.loginserverpackets.ChangePasswordResponse;
import com.l2jserver.gameserver.network.loginserverpackets.InitLS;
import com.l2jserver.gameserver.network.loginserverpackets.KickPlayer;
import com.l2jserver.gameserver.network.loginserverpackets.LoginServerFail;
import com.l2jserver.gameserver.network.loginserverpackets.PlayerAuthResponse;
import com.l2jserver.gameserver.network.loginserverpackets.RequestCharacters;
import com.l2jserver.gameserver.network.serverpackets.CharSelectionInfo;
import com.l2jserver.gameserver.network.serverpackets.LoginFail;
import com.l2jserver.gameserver.network.serverpackets.SystemMessage;
import com.l2jserver.util.Rnd;
import com.l2jserver.util.Util;
import com.l2jserver.util.crypt.NewCrypt;
import com.l2jserver.util.network.BaseSendablePacket;
public class LoginServerThread extends Thread
{
protected static final Logger _log = LoggerFactory.getLogger(LoginServerThread.class);
protected static final Logger _logAccounting = LoggerFactory.getLogger("accounting");
/** @see com.l2jserver.loginserver.L2LoginServer#PROTOCOL_REV */
private static final int REVISION = 0x0106;
private final String _hostname;
private final int _port;
private final int _gamePort;
private Socket _loginSocket;
private OutputStream _out;
/**
* The BlowFish engine used to encrypt packets
* It is first initialized with a unified key:
* "_;v.]05-31!|+-%xT!^[$\00"
*
* and then after handshake, with a new key sent by
* login server during the handshake. This new key is stored
* in blowfishKey
*/
private NewCrypt _blowfish;
private byte[] _hexID;
private final boolean _acceptAlternate;
private int _requestID;
private final boolean _reserveHost;
private int _maxPlayer;
private final List _waitingClients;
private final Map _accountsInGameServer = new ConcurrentHashMap<>();
private int _status;
private String _serverName;
private final List _subnets;
private final List _hosts;
/**
* Instantiates a new login server thread.
*/
protected LoginServerThread()
{
super("LoginServerThread");
_port = Config.GAME_SERVER_LOGIN_PORT;
_gamePort = Config.PORT_GAME;
_hostname = Config.GAME_SERVER_LOGIN_HOST;
_hexID = Config.HEX_ID;
if (_hexID == null)
{
_requestID = Config.REQUEST_ID;
_hexID = Util.generateHex(16);
}
else
{
_requestID = Config.SERVER_ID;
}
_acceptAlternate = Config.ACCEPT_ALTERNATE_ID;
_reserveHost = Config.RESERVE_HOST_ON_LOGIN;
_subnets = Config.GAME_SERVER_SUBNETS;
_hosts = Config.GAME_SERVER_HOSTS;
_waitingClients = new CopyOnWriteArrayList<>();
_maxPlayer = Config.MAXIMUM_ONLINE_USERS;
}
@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);
InputStream in = _loginSocket.getInputStream();
_out = new BufferedOutputStream(_loginSocket.getOutputStream());
// init Blowfish
byte[] blowfishKey = Util.generateHex(40);
// Protect the new blowfish key what cannot begin with zero
if (blowfishKey[0] == 0)
{
blowfishKey[0] = (byte) Rnd.get(32, 64);
}
_blowfish = new NewCrypt("_;v.]05-31!|+-%xT!^[$\00");
while (!isInterrupted())
{
lengthLo = in.read();
lengthHi = in.read();
length = (lengthHi * 256) + lengthLo;
if (lengthHi < 0)
{
_log.info("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
_blowfish.decrypt(incoming, 0, incoming.length);
checksumOk = NewCrypt.verifyChecksum(incoming);
if (!checksumOk)
{
_log.warn("Incorrect packet checksum, ignoring packet (LS)");
break;
}
int packetType = incoming[0] & 0xff;
switch (packetType)
{
case 0x00:
InitLS init = new InitLS(incoming);
if (init.getRevision() != REVISION)
{
// TODO: revision mismatch
_log.warn("/!\\ Revision mismatch between LS and GS /!\\");
break;
}
RSAPublicKey publicKey;
try
{
KeyFactory kfac = KeyFactory.getInstance("RSA");
BigInteger modulus = new BigInteger(init.getRSAKey());
RSAPublicKeySpec kspec1 = new RSAPublicKeySpec(modulus, RSAKeyGenParameterSpec.F4);
publicKey = (RSAPublicKey) kfac.generatePublic(kspec1);
}
catch (GeneralSecurityException e)
{
_log.warn("Trouble while init the public key send by login");
break;
}
// send the blowfish key through the rsa encryption
sendPacket(new BlowFishKey(blowfishKey, publicKey));
// now, only accept packet with the new encryption
_blowfish = new NewCrypt(blowfishKey);
sendPacket(new AuthRequest(_requestID, _acceptAlternate, _hexID, _gamePort, _reserveHost, _maxPlayer, _subnets, _hosts));
break;
case 0x01:
LoginServerFail lsf = new LoginServerFail(incoming);
_log.info("Damn! Registeration Failed: {}", lsf.getReasonString());
// login will close the connection here
break;
case 0x02:
AuthResponse aresp = new AuthResponse(incoming);
int serverID = aresp.getServerId();
_serverName = aresp.getServerName();
Config.saveHexid(serverID, hexToString(_hexID));
_log.info("Registered on login as Server {}: {}", serverID, _serverName);
ServerStatus st = new ServerStatus();
if (Config.SERVER_LIST_BRACKET)
{
st.addAttribute(ServerStatus.SERVER_LIST_SQUARE_BRACKET, ServerStatus.ON);
}
else
{
st.addAttribute(ServerStatus.SERVER_LIST_SQUARE_BRACKET, ServerStatus.OFF);
}
st.addAttribute(ServerStatus.SERVER_TYPE, Config.SERVER_LIST_TYPE);
if (Config.SERVER_GMONLY)
{
st.addAttribute(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_GM_ONLY);
}
else
{
st.addAttribute(ServerStatus.SERVER_LIST_STATUS, ServerStatus.STATUS_AUTO);
}
if (Config.SERVER_LIST_AGE == 15)
{
st.addAttribute(ServerStatus.SERVER_AGE, ServerStatus.SERVER_AGE_15);
}
else if (Config.SERVER_LIST_AGE == 18)
{
st.addAttribute(ServerStatus.SERVER_AGE, ServerStatus.SERVER_AGE_18);
}
else
{
st.addAttribute(ServerStatus.SERVER_AGE, ServerStatus.SERVER_AGE_ALL);
}
sendPacket(st);
if (L2World.getInstance().getAllPlayersCount() > 0)
{
final List playerList = new ArrayList<>();
for (L2PcInstance player : L2World.getInstance().getPlayers())
{
playerList.add(player.getAccountName());
}
sendPacket(new PlayerInGame(playerList));
}
break;
case 0x03:
PlayerAuthResponse par = new PlayerAuthResponse(incoming);
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())
{
PlayerInGame pig = new PlayerInGame(par.getAccount());
sendPacket(pig);
wcToRemove.gameClient.setState(GameClientState.AUTHED);
wcToRemove.gameClient.setSessionId(wcToRemove.session);
CharSelectionInfo cl = new CharSelectionInfo(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 for account {}.", wcToRemove.account);
// wcToRemove.gameClient.getConnection().sendPacket(new LoginFail(LoginFail.SYSTEM_ERROR_LOGIN_LATER));
wcToRemove.gameClient.close(new LoginFail(LoginFail.SYSTEM_ERROR_LOGIN_LATER));
_accountsInGameServer.remove(wcToRemove.account);
}
_waitingClients.remove(wcToRemove);
}
break;
case 0x04:
KickPlayer kp = new KickPlayer(incoming);
doKickPlayer(kp.getAccount());
break;
case 0x05:
RequestCharacters rc = new RequestCharacters(incoming);
getCharsOnServer(rc.getAccount());
break;
case 0x06:
new ChangePasswordResponse(incoming);
break;
}
}
}
catch (UnknownHostException e)
{
_log.warn("Unknown host!", e);
}
catch (SocketException e)
{
_log.warn("LoginServer not avaible, trying to reconnect...");
}
catch (IOException e)
{
_log.warn("Disconnected from Login, Trying to reconnect!", e);
}
finally
{
try
{
_loginSocket.close();
if (isInterrupted())
{
return;
}
}
catch (Exception e)
{
}
}
try
{
Thread.sleep(5000); // 5 seconds tempo.
}
catch (InterruptedException e)
{
return; // never swallow an interrupt!
}
}
}
/**
* Adds the waiting client and send request.
* @param acc the account
* @param client the game client
* @param key the session key
*/
public void addWaitingClientAndSendRequest(String acc, L2GameClient client, SessionKey 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!");
}
}
/**
* Removes the waiting client.
* @param client the client
*/
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);
}
}
}
/**
* Send logout for the given account.
* @param account the account
*/
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!");
}
finally
{
_accountsInGameServer.remove(account);
}
}
/**
* Adds the game server login.
* @param account the account
* @param client the client
* @return {@code true} if account was not already logged in, {@code false} otherwise
*/
public boolean addGameServerLogin(String account, L2GameClient client)
{
return _accountsInGameServer.putIfAbsent(account, client) == null;
}
/**
* Send access level.
* @param account the account
* @param level the access level
*/
public void sendAccessLevel(String account, int level)
{
ChangeAccessLevel cal = new ChangeAccessLevel(account, level);
try
{
sendPacket(cal);
}
catch (IOException e)
{
}
}
/**
* Send client tracert.
* @param account the account
* @param address the address
*/
public void sendClientTracert(String account, String[] address)
{
PlayerTracert ptc = new PlayerTracert(account, address[0], address[1], address[2], address[3], address[4]);
try
{
sendPacket(ptc);
}
catch (IOException e)
{
}
}
/**
* Send mail.
* @param account the account
* @param mailId the mail id
* @param args the args
*/
public void sendMail(String account, String mailId, String... args)
{
SendMail sem = new SendMail(account, mailId, args);
try
{
sendPacket(sem);
}
catch (IOException e)
{
}
}
/**
* Send temp ban.
* @param account the account
* @param ip the ip
* @param time the time
*/
public void sendTempBan(String account, String ip, long time)
{
TempBan tbn = new TempBan(account, ip, time);
try
{
sendPacket(tbn);
}
catch (IOException e)
{
}
}
/**
* Hex to string.
* @param hex the hex value
* @return the hex value as string
*/
private String hexToString(byte[] hex)
{
return new BigInteger(hex).toString(16);
}
/**
* Kick player for the given account.
* @param account the account
*/
public void doKickPlayer(String account)
{
L2GameClient client = _accountsInGameServer.get(account);
if (client != null)
{
_logAccounting.warn("Kicked by login: {}", client);
client.setAditionalClosePacket(SystemMessage.getSystemMessage(SystemMessageId.ANOTHER_LOGIN_WITH_ACCOUNT));
client.closeNow();
}
}
/**
* Gets the chars on server.
* @param account the account
*/
private void getCharsOnServer(String account)
{
int chars = 0;
List charToDel = new ArrayList<>();
try (Connection con = ConnectionFactory.getInstance().getConnection();
PreparedStatement ps = con.prepareStatement("SELECT deletetime FROM characters WHERE account_name=?"))
{
ps.setString(1, account);
try (ResultSet rs = ps.executeQuery())
{
while (rs.next())
{
chars++;
long delTime = rs.getLong("deletetime");
if (delTime != 0)
{
charToDel.add(delTime);
}
}
}
}
catch (SQLException e)
{
_log.warn("Exception: getCharsOnServer!", e);
}
ReplyCharacters rec = new ReplyCharacters(account, chars, charToDel);
try
{
sendPacket(rec);
}
catch (IOException e)
{
}
}
/**
* Send packet.
* @param sl the sendable packet
* @throws IOException Signals that an I/O exception has occurred.
*/
private void sendPacket(BaseSendablePacket sl) throws IOException
{
byte[] data = sl.getContent();
NewCrypt.appendChecksum(data);
_blowfish.crypt(data, 0, data.length);
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();
}
}
/**
* Sets the max player.
* @param maxPlayer The maxPlayer to set.
*/
public void setMaxPlayer(int maxPlayer)
{
sendServerStatus(ServerStatus.MAX_PLAYERS, maxPlayer);
_maxPlayer = maxPlayer;
}
/**
* Gets the max player.
* @return Returns the maxPlayer.
*/
public int getMaxPlayer()
{
return _maxPlayer;
}
/**
* Send server status.
* @param id the id
* @param value the value
*/
public void sendServerStatus(int id, int value)
{
ServerStatus ss = new ServerStatus();
ss.addAttribute(id, value);
try
{
sendPacket(ss);
}
catch (IOException e)
{
}
}
/**
* Send Server Type Config to LS.
*/
public void sendServerType()
{
ServerStatus ss = new ServerStatus();
ss.addAttribute(ServerStatus.SERVER_TYPE, Config.SERVER_LIST_TYPE);
try
{
sendPacket(ss);
}
catch (IOException e)
{
}
}
/**
* Send change password.
* @param accountName the account name
* @param charName the char name
* @param oldpass the old pass
* @param newpass the new pass
*/
public void sendChangePassword(String accountName, String charName, String oldpass, String newpass)
{
ChangePassword cp = new ChangePassword(accountName, charName, oldpass, newpass);
try
{
sendPacket(cp);
}
catch (IOException e)
{
}
}
/**
* Gets the status string.
* @return the status string
*/
public String getStatusString()
{
return ServerStatus.STATUS_STRING[_status];
}
/**
* Gets the server name.
* @return the server name.
*/
public String getServerName()
{
return _serverName;
}
/**
* Sets the server status.
* @param status the new server status
*/
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 L2GameClient getClient(String name)
{
return name != null ? _accountsInGameServer.get(name) : null;
}
public static class SessionKey
{
public int playOkID1;
public int playOkID2;
public int loginOkID1;
public int loginOkID2;
/**
* Instantiates a new session key.
* @param loginOK1 the login o k1
* @param loginOK2 the login o k2
* @param playOK1 the play o k1
* @param playOK2 the play o k2
*/
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 static class WaitingClient
{
public String account;
public L2GameClient gameClient;
public SessionKey session;
/**
* Instantiates a new waiting client.
* @param acc the acc
* @param client the client
* @param key the key
*/
public WaitingClient(String acc, L2GameClient client, SessionKey key)
{
account = acc;
gameClient = client;
session = key;
}
}
/**
* Gets the single instance of LoginServerThread.
* @return single instance of LoginServerThread
*/
public static LoginServerThread getInstance()
{
return SingletonHolder._instance;
}
private static class SingletonHolder
{
protected static final LoginServerThread _instance = new LoginServerThread();
}
}