612 lines
18 KiB
C++
Executable File
612 lines
18 KiB
C++
Executable File
#include "server.h"
|
|
|
|
#include "acl_roles_handler.h"
|
|
#include "aoclient.h"
|
|
#include "area_data.h"
|
|
#include "command_extension.h"
|
|
#include "config_manager.h"
|
|
#include "db_manager.h"
|
|
#include "logger/u_logger.h"
|
|
#include "music_manager.h"
|
|
#include "network/network_socket.h"
|
|
#include "packet/packet_factory.h"
|
|
#include "serverpublisher.h"
|
|
|
|
Server::Server(int p_ws_port, int p_wss_port, QObject *parent) :
|
|
QObject(parent),
|
|
m_port(p_ws_port),
|
|
m_secure_port(p_wss_port),
|
|
m_player_count(0)
|
|
{
|
|
timer = new QTimer(this);
|
|
|
|
db_manager = new DBManager;
|
|
|
|
acl_roles_handler = new ACLRolesHandler(this);
|
|
acl_roles_handler->loadFile("config/acl_roles.ini");
|
|
|
|
command_extension_collection = new CommandExtensionCollection;
|
|
command_extension_collection->setCommandNameWhitelist(AOClient::COMMANDS.keys());
|
|
command_extension_collection->loadFile("config/command_extensions.ini");
|
|
|
|
logger = new ULogger(this);
|
|
connect(this, &Server::logConnectionAttempt, logger, &ULogger::logConnectionAttempt);
|
|
|
|
AOPacket::registerPackets();
|
|
|
|
// Initialize both servers
|
|
server = new QWebSocketServer(QStringLiteral("Akashi"), QWebSocketServer::NonSecureMode, this);
|
|
secure_server = new QWebSocketServer(QStringLiteral("Akashi"), QWebSocketServer::SecureMode, this);
|
|
}
|
|
|
|
void Server::start()
|
|
{
|
|
QString bind_ip = ConfigManager::bindIP();
|
|
QHostAddress bind_addr;
|
|
|
|
if (bind_ip == "all")
|
|
bind_addr = QHostAddress::Any;
|
|
else
|
|
bind_addr = QHostAddress(bind_ip);
|
|
if (bind_addr.protocol() != QAbstractSocket::IPv4Protocol && bind_addr.protocol() != QAbstractSocket::IPv6Protocol && bind_addr != QHostAddress::Any) {
|
|
qDebug() << bind_ip << "is an invalid IP address to listen on! Server not starting, check your config.";
|
|
return;
|
|
}
|
|
|
|
// Configure SSL for secure server
|
|
QSettings settings("config/ssl.ini", QSettings::IniFormat);
|
|
|
|
QString privateKeyPath = settings.value("SSL/privateKey").toString();
|
|
QString certificatePath = settings.value("SSL/certificate").toString();
|
|
QString caCertificatePath = settings.value("SSL/caCertificate").toString();
|
|
|
|
QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
|
|
|
|
QFile keyFile(privateKeyPath);
|
|
if (!keyFile.open(QIODevice::ReadOnly)) {
|
|
qWarning() << "Unable to open private key file.";
|
|
}
|
|
else {
|
|
QSslKey privateKey;
|
|
privateKey = QSslKey(&keyFile, QSsl::Ec);
|
|
if (privateKey.isNull()) {
|
|
keyFile.reset();
|
|
privateKey = QSslKey(&keyFile, QSsl::Rsa);
|
|
}
|
|
keyFile.close();
|
|
|
|
QFile certFile(certificatePath);
|
|
if (!certFile.open(QIODevice::ReadOnly)) {
|
|
qWarning() << "Unable to open certificate file.";
|
|
}
|
|
else {
|
|
QList<QSslCertificate> certs = QSslCertificate::fromData(certFile.readAll());
|
|
certFile.close();
|
|
|
|
QFile caFile(caCertificatePath);
|
|
if (caFile.open(QIODevice::ReadOnly)) {
|
|
QSslCertificate caCertificate(&caFile);
|
|
sslConfig.addCaCertificate(caCertificate);
|
|
caFile.close();
|
|
}
|
|
|
|
sslConfig.setPrivateKey(privateKey);
|
|
if (!certs.isEmpty()) {
|
|
sslConfig.setLocalCertificate(certs.first());
|
|
if (certs.size() > 1)
|
|
sslConfig.setCaCertificates(certs.mid(1));
|
|
}
|
|
secure_server->setSslConfiguration(sslConfig);
|
|
}
|
|
}
|
|
|
|
// Start non-secure server
|
|
if (!server->listen(bind_addr, m_port)) {
|
|
qDebug() << "Non-secure server error:" << server->errorString();
|
|
} else {
|
|
connect(server, &QWebSocketServer::newConnection, this, [this]() {
|
|
this->clientConnected(server);
|
|
});
|
|
qInfo() << "Non-secure server listening on" << server->serverPort();
|
|
}
|
|
|
|
// Start secure server
|
|
if (!secure_server->listen(bind_addr, m_secure_port)) {
|
|
qDebug() << "Secure server error:" << secure_server->errorString();
|
|
} else {
|
|
connect(secure_server, &QWebSocketServer::newConnection, this, [this]() {
|
|
this->clientConnected(secure_server);
|
|
});
|
|
qInfo() << "Secure server listening on" << secure_server->serverPort();
|
|
}
|
|
|
|
// Construct modern advertiser if enabled in config
|
|
server_publisher = new ServerPublisher(server->serverPort(), &m_player_count, this);
|
|
|
|
// Get characters from config file
|
|
m_characters = ConfigManager::charlist();
|
|
|
|
// Get backgrounds from config file
|
|
m_backgrounds = ConfigManager::backgrounds();
|
|
|
|
// Build our music manager.
|
|
MusicList l_musiclist = ConfigManager::musiclist();
|
|
music_manager = new MusicManager(ConfigManager::cdnList(), l_musiclist, ConfigManager::ordered_songs(), this);
|
|
connect(music_manager, &MusicManager::sendFMPacket, this, &Server::unicast);
|
|
connect(music_manager, &MusicManager::sendAreaFMPacket, this, QOverload<AOPacket *, int>::of(&Server::broadcast));
|
|
|
|
// Get musiclist from config file
|
|
m_music_list = music_manager->rootMusiclist();
|
|
|
|
// Assembles the area list
|
|
m_area_names = ConfigManager::sanitizedAreaNames();
|
|
for (int i = 0; i < m_area_names.length(); i++) {
|
|
QString area_name = QString::number(i) + ":" + m_area_names[i];
|
|
AreaData *l_area = new AreaData(area_name, i, music_manager);
|
|
m_areas.insert(i, l_area);
|
|
connect(l_area, &AreaData::sendAreaPacket, this, QOverload<AOPacket *, int>::of(&Server::broadcast));
|
|
connect(l_area, &AreaData::sendAreaPacketClient, this, &Server::unicast);
|
|
connect(l_area, &AreaData::userJoinedArea, music_manager, &MusicManager::userJoinedArea);
|
|
music_manager->registerArea(i);
|
|
// Search for and load inventory based on the area name
|
|
loadInventoryForArea(l_area, m_area_names[i]);
|
|
}
|
|
|
|
// Loads the command help information. This is not stored inside the server.
|
|
ConfigManager::loadCommandHelp();
|
|
|
|
// Get IP bans
|
|
m_ipban_list = ConfigManager::iprangeBans();
|
|
|
|
// Rate-Limiter for IC-Chat
|
|
m_message_floodguard_timer = new QTimer(this);
|
|
m_message_floodguard_timer->setSingleShot(true);
|
|
connect(m_message_floodguard_timer, &QTimer::timeout, this, &Server::allowMessage);
|
|
|
|
// Prepare player IDs and reference hash.
|
|
for (int i = ConfigManager::maxPlayers() - 1; i >= 0; i--) {
|
|
m_available_ids.push(i);
|
|
m_clients_ids.insert(i, nullptr);
|
|
}
|
|
}
|
|
|
|
void Server::loadInventoryForArea(AreaData* area, const QString& areaName)
|
|
{
|
|
// Set up the directory where inventory files are stored.
|
|
QString inventoryDirectory = "storage/inventory"; // Adjust this path as necessary
|
|
|
|
QDir dir(inventoryDirectory);
|
|
if (!dir.exists()) {
|
|
qWarning() << "Inventory directory does not exist!";
|
|
return;
|
|
}
|
|
|
|
// Search for an inventory file corresponding to the area name (e.g., "area_1.ini").
|
|
QString inventoryFileName = (areaName + ".ini").toLower().replace("..", ""); // Sanitize the area name, assuming file is named as area name
|
|
QString filePath = dir.absoluteFilePath(inventoryFileName);
|
|
|
|
if (QFile::exists(filePath)) {
|
|
qDebug() << "Loading inventory for area:" << areaName;
|
|
|
|
// Load the inventory from the .ini file
|
|
QSettings settings(filePath, QSettings::IniFormat);
|
|
area->loadInventory(settings); // Assuming the AreaData class has a `loadInventory()` method
|
|
} else {
|
|
qWarning() << "Inventory file not found for area:" << areaName;
|
|
}
|
|
}
|
|
|
|
QVector<AOClient *> Server::getClients()
|
|
{
|
|
return m_clients;
|
|
}
|
|
|
|
void Server::clientConnected(QWebSocketServer* whichServer)
|
|
{
|
|
QWebSocket *socket = whichServer->nextPendingConnection();
|
|
if (!socket) {
|
|
qWarning() << "Failed to get pending connection from" << whichServer->serverName();
|
|
return;
|
|
}
|
|
|
|
NetworkSocket *l_socket = new NetworkSocket(socket, socket);
|
|
|
|
// Too many players. Reject connection!
|
|
// This also enforces the maximum playercount.
|
|
if (m_available_ids.empty()) {
|
|
AOPacket *disconnect_reason = PacketFactory::createPacket("BD", {"Maximum playercount has been reached."});
|
|
l_socket->write(disconnect_reason);
|
|
l_socket->close();
|
|
l_socket->deleteLater();
|
|
return;
|
|
}
|
|
|
|
int user_id = m_available_ids.pop();
|
|
AOClient *client = new AOClient(this, l_socket, l_socket, user_id, music_manager);
|
|
m_clients_ids.insert(user_id, client);
|
|
m_player_state_observer.registerClient(client);
|
|
|
|
int multiclient_count = 1;
|
|
bool is_at_multiclient_limit = false;
|
|
client->calculateIpid();
|
|
auto ban = db_manager->isIPBanned(client->getIpid());
|
|
bool is_banned = ban.first;
|
|
for (AOClient *joined_client : qAsConst(m_clients)) {
|
|
if (client->m_remote_ip.isEqual(joined_client->m_remote_ip))
|
|
multiclient_count++;
|
|
}
|
|
|
|
if (multiclient_count > ConfigManager::multiClientLimit() && !client->m_remote_ip.isLoopback())
|
|
is_at_multiclient_limit = true;
|
|
|
|
if (is_banned) {
|
|
QString ban_duration;
|
|
if (!(ban.second.duration == -2)) {
|
|
ban_duration = QDateTime::fromSecsSinceEpoch(ban.second.time).addSecs(ban.second.duration).toString("MM/dd/yyyy, hh:mm");
|
|
}
|
|
else {
|
|
ban_duration = "Permanently.";
|
|
}
|
|
AOPacket *ban_reason = PacketFactory::createPacket("BD", {"Reason: " + ban.second.reason + "\nBan ID: " + QString::number(ban.second.id) + "\nUntil: " + ban_duration});
|
|
socket->sendTextMessage(ban_reason->toUtf8());
|
|
}
|
|
if (is_banned || is_at_multiclient_limit) {
|
|
client->deleteLater();
|
|
l_socket->close(QWebSocketProtocol::CloseCodeNormal);
|
|
markIDFree(user_id);
|
|
return;
|
|
}
|
|
|
|
QHostAddress l_remote_ip = client->m_remote_ip;
|
|
if (l_remote_ip.protocol() == QAbstractSocket::IPv6Protocol) {
|
|
l_remote_ip = parseToIPv4(l_remote_ip);
|
|
}
|
|
|
|
if (isIPBanned(l_remote_ip)) {
|
|
QString l_reason = "Your IP has been banned by a moderator.";
|
|
AOPacket *l_ban_reason = PacketFactory::createPacket("BD", {l_reason});
|
|
l_socket->write(l_ban_reason);
|
|
client->deleteLater();
|
|
l_socket->close(QWebSocketProtocol::CloseCodeNormal);
|
|
markIDFree(user_id);
|
|
return;
|
|
}
|
|
|
|
m_clients.append(client);
|
|
connect(l_socket, &NetworkSocket::clientDisconnected, this, [=] {
|
|
if (client->hasJoined()) {
|
|
decreasePlayerCount();
|
|
}
|
|
m_clients.removeAll(client);
|
|
l_socket->deleteLater();
|
|
});
|
|
connect(l_socket, &NetworkSocket::handlePacket, client, &AOClient::handlePacket);
|
|
|
|
// This is the infamous workaround for
|
|
// tsuserver4. It should disable fantacrypt
|
|
// completely in any client 2.4.3 or newer
|
|
AOPacket *decryptor = PacketFactory::createPacket("decryptor", {"NOENCRYPT"});
|
|
client->sendPacket(decryptor);
|
|
hookupAOClient(client);
|
|
}
|
|
|
|
void Server::updateCharsTaken(AreaData *area)
|
|
{
|
|
QStringList chars_taken;
|
|
for (const QString &cur_char : qAsConst(m_characters)) {
|
|
chars_taken.append(area->charactersTaken().contains(getCharID(cur_char))
|
|
? QStringLiteral("-1")
|
|
: QStringLiteral("0"));
|
|
}
|
|
|
|
AOPacket *response_cc = PacketFactory::createPacket("CharsCheck", chars_taken);
|
|
|
|
for (AOClient *l_client : qAsConst(m_clients)) {
|
|
if (l_client->areaId() == area->index()) {
|
|
if (!l_client->m_is_charcursed)
|
|
l_client->sendPacket(response_cc);
|
|
else {
|
|
QStringList chars_taken_cursed = getCursedCharsTaken(l_client, chars_taken);
|
|
AOPacket *response_cc_cursed = PacketFactory::createPacket("CharsCheck", chars_taken_cursed);
|
|
l_client->sendPacket(response_cc_cursed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
QStringList Server::getCursedCharsTaken(AOClient *client, QStringList chars_taken)
|
|
{
|
|
QStringList chars_taken_cursed;
|
|
for (int i = 0; i < chars_taken.length(); i++) {
|
|
if (!client->m_charcurse_list.contains(i))
|
|
chars_taken_cursed.append("-1");
|
|
else
|
|
chars_taken_cursed.append(chars_taken.value(i));
|
|
}
|
|
return chars_taken_cursed;
|
|
}
|
|
|
|
bool Server::isMessageAllowed() const
|
|
{
|
|
return m_can_send_ic_messages;
|
|
}
|
|
|
|
void Server::startMessageFloodguard(int f_duration)
|
|
{
|
|
m_can_send_ic_messages = false;
|
|
m_message_floodguard_timer->start(f_duration);
|
|
}
|
|
|
|
QHostAddress Server::parseToIPv4(QHostAddress f_remote_ip)
|
|
{
|
|
bool l_ok;
|
|
QHostAddress l_remote_ip = f_remote_ip;
|
|
QHostAddress l_temp_remote_ip = QHostAddress(f_remote_ip.toIPv4Address(&l_ok));
|
|
if (l_ok) {
|
|
l_remote_ip = l_temp_remote_ip;
|
|
}
|
|
return l_remote_ip;
|
|
}
|
|
|
|
void Server::reloadSettings()
|
|
{
|
|
ConfigManager::reloadSettings();
|
|
emit reloadRequest(ConfigManager::serverName(), ConfigManager::serverDescription());
|
|
emit updateHTTPConfiguration();
|
|
logger->loadLogtext();
|
|
m_ipban_list = ConfigManager::iprangeBans();
|
|
acl_roles_handler->loadFile("config/acl_roles.ini");
|
|
command_extension_collection->loadFile("config/command_extensions.ini");
|
|
}
|
|
|
|
void Server::broadcast(AOPacket *packet, int area_index)
|
|
{
|
|
QVector<int> l_client_ids = m_areas.value(area_index)->joinedIDs();
|
|
for (const int l_client_id : qAsConst(l_client_ids)) {
|
|
getClientByID(l_client_id)->sendPacket(packet);
|
|
}
|
|
}
|
|
|
|
void Server::broadcast(AOPacket *packet)
|
|
{
|
|
for (AOClient *l_client : qAsConst(m_clients)) {
|
|
l_client->sendPacket(packet);
|
|
}
|
|
}
|
|
|
|
void Server::broadcast(AOPacket *packet, TARGET_TYPE target)
|
|
{
|
|
for (AOClient *l_client : qAsConst(m_clients)) {
|
|
switch (target) {
|
|
case TARGET_TYPE::MODCHAT:
|
|
if (l_client->checkPermission(ACLRole::MODCHAT)) {
|
|
l_client->sendPacket(packet);
|
|
}
|
|
break;
|
|
case TARGET_TYPE::ADVERT:
|
|
if (l_client->m_advert_enabled) {
|
|
l_client->sendPacket(packet);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Server::broadcast(AOPacket *packet, AOPacket *other_packet, TARGET_TYPE target)
|
|
{
|
|
switch (target) {
|
|
case TARGET_TYPE::AUTHENTICATED:
|
|
for (AOClient *l_client : qAsConst(m_clients)) {
|
|
if (l_client->isAuthenticated()) {
|
|
l_client->sendPacket(other_packet);
|
|
}
|
|
else {
|
|
l_client->sendPacket(packet);
|
|
}
|
|
}
|
|
default:
|
|
// Unimplemented, so not handled.
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Server::unicast(AOPacket *f_packet, int f_client_id)
|
|
{
|
|
AOClient *l_client = getClientByID(f_client_id);
|
|
if (l_client != nullptr) { // This should never happen, but safety first.
|
|
l_client->sendPacket(f_packet);
|
|
return;
|
|
}
|
|
}
|
|
|
|
QList<AOClient *> Server::getClientsByIpid(QString ipid)
|
|
{
|
|
QList<AOClient *> return_clients;
|
|
for (AOClient *l_client : qAsConst(m_clients)) {
|
|
if (l_client->getIpid() == ipid)
|
|
return_clients.append(l_client);
|
|
}
|
|
return return_clients;
|
|
}
|
|
|
|
QList<AOClient *> Server::getClientsByHwid(QString f_hwid)
|
|
{
|
|
QList<AOClient *> return_clients;
|
|
for (AOClient *l_client : qAsConst(m_clients)) {
|
|
if (l_client->getHwid() == f_hwid)
|
|
return_clients.append(l_client);
|
|
}
|
|
return return_clients;
|
|
}
|
|
|
|
AOClient *Server::getClientByID(int id)
|
|
{
|
|
return m_clients_ids.value(id);
|
|
}
|
|
|
|
int Server::getPlayerCount()
|
|
{
|
|
return m_player_count;
|
|
}
|
|
|
|
QStringList Server::getCharacters()
|
|
{
|
|
return m_characters;
|
|
}
|
|
|
|
int Server::getCharacterCount()
|
|
{
|
|
return m_characters.length();
|
|
}
|
|
|
|
QString Server::getCharacterById(int f_chr_id)
|
|
{
|
|
QString l_chr;
|
|
|
|
if (f_chr_id >= 0 && f_chr_id < m_characters.length()) {
|
|
l_chr = m_characters.at(f_chr_id);
|
|
}
|
|
|
|
return l_chr;
|
|
}
|
|
|
|
int Server::getCharID(QString char_name)
|
|
{
|
|
for (int i = 0; i < m_characters.length(); i++) {
|
|
if (m_characters[i].toLower() == char_name.toLower()) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1; // character does not exist
|
|
}
|
|
|
|
QVector<AreaData *> Server::getAreas()
|
|
{
|
|
return m_areas;
|
|
}
|
|
|
|
int Server::getAreaCount()
|
|
{
|
|
return m_areas.length();
|
|
}
|
|
|
|
AreaData *Server::getAreaById(int f_area_id)
|
|
{
|
|
AreaData *l_area = nullptr;
|
|
|
|
if (f_area_id >= 0 && f_area_id < m_areas.length()) {
|
|
l_area = m_areas.at(f_area_id);
|
|
}
|
|
|
|
return l_area;
|
|
}
|
|
|
|
QQueue<QString> Server::getAreaBuffer(const QString &f_areaName)
|
|
{
|
|
return logger->buffer(f_areaName);
|
|
}
|
|
|
|
QStringList Server::getAreaNames()
|
|
{
|
|
return m_area_names;
|
|
}
|
|
|
|
QString Server::getAreaName(int f_area_id)
|
|
{
|
|
QString l_name;
|
|
|
|
if (f_area_id >= 0 && f_area_id < m_area_names.length()) {
|
|
l_name = m_area_names.at(f_area_id);
|
|
}
|
|
|
|
return l_name;
|
|
}
|
|
|
|
QStringList Server::getMusicList()
|
|
{
|
|
return m_music_list;
|
|
}
|
|
|
|
QStringList Server::getBackgrounds()
|
|
{
|
|
return m_backgrounds;
|
|
}
|
|
|
|
DBManager *Server::getDatabaseManager()
|
|
{
|
|
return db_manager;
|
|
}
|
|
|
|
ACLRolesHandler *Server::getACLRolesHandler()
|
|
{
|
|
return acl_roles_handler;
|
|
}
|
|
|
|
CommandExtensionCollection *Server::getCommandExtensionCollection()
|
|
{
|
|
return command_extension_collection;
|
|
}
|
|
|
|
void Server::allowMessage()
|
|
{
|
|
m_can_send_ic_messages = true;
|
|
}
|
|
|
|
void Server::markIDFree(const int &f_user_id)
|
|
{
|
|
m_player_state_observer.unregisterClient(m_clients_ids[f_user_id]);
|
|
m_clients_ids.insert(f_user_id, nullptr);
|
|
m_available_ids.push(f_user_id);
|
|
}
|
|
|
|
void Server::hookupAOClient(AOClient *client)
|
|
{
|
|
connect(client, &AOClient::joined, this, &Server::increasePlayerCount);
|
|
connect(client, &AOClient::logIC, logger, &ULogger::logIC);
|
|
connect(client, &AOClient::logOOC, logger, &ULogger::logOOC);
|
|
connect(client, &AOClient::logLogin, logger, &ULogger::logLogin);
|
|
connect(client, &AOClient::logCMD, logger, &ULogger::logCMD);
|
|
connect(client, &AOClient::logBan, logger, &ULogger::logBan);
|
|
connect(client, &AOClient::logKick, logger, &ULogger::logKick);
|
|
connect(client, &AOClient::logModcall, logger, &ULogger::logModcall);
|
|
connect(client, &AOClient::clientSuccessfullyDisconnected, this, &Server::markIDFree);
|
|
}
|
|
|
|
void Server::increasePlayerCount()
|
|
{
|
|
m_player_count++;
|
|
emit playerCountUpdated(m_player_count);
|
|
}
|
|
|
|
void Server::decreasePlayerCount()
|
|
{
|
|
m_player_count--;
|
|
emit playerCountUpdated(m_player_count);
|
|
}
|
|
|
|
bool Server::isIPBanned(QHostAddress f_remote_IP)
|
|
{
|
|
bool l_match_found = false;
|
|
for (const QString &l_ipban : qAsConst(m_ipban_list)) {
|
|
if (f_remote_IP.isInSubnet(QHostAddress::parseSubnet(l_ipban))) {
|
|
l_match_found = true;
|
|
break;
|
|
}
|
|
}
|
|
return l_match_found;
|
|
}
|
|
|
|
Server::~Server()
|
|
{
|
|
for (AOClient *l_client : qAsConst(m_clients)) {
|
|
l_client->deleteLater();
|
|
}
|
|
server->deleteLater();
|
|
acl_roles_handler->deleteLater();
|
|
|
|
delete db_manager;
|
|
}
|