////////////////////////////////////////////////////////////////////////////////////// // akashi - a server for Attorney Online 2 // // Copyright (C) 2020 scatterflower // // // // This program is free software: you can redistribute it and/or modify // // it under the terms of the GNU Affero 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 Affero General Public License for more details. // // // // You should have received a copy of the GNU Affero General Public License // // along with this program. If not, see . // ////////////////////////////////////////////////////////////////////////////////////// #include "include/aoclient.h" AOClient::AOClient(Server* p_server, QTcpSocket* p_socket, QObject* parent, int user_id) : QObject(parent) { socket = p_socket; server = p_server; id = user_id; joined = false; password = ""; current_area = 0; current_char = ""; remote_ip = p_socket->peerAddress(); is_partial = false; last_wtce_time = 0; last_message = ""; } void AOClient::clientData() { QString data = QString::fromUtf8(socket->readAll()); if (is_partial) { data = partial_packet + data; } if (!data.endsWith("%")) { is_partial = true; } QStringList all_packets = data.split("%"); all_packets.removeLast(); // Remove the entry after the last delimiter for (QString single_packet : all_packets) { AOPacket packet(single_packet); handlePacket(packet); } } void AOClient::clientDisconnected() { #ifdef NET_DEBUG qDebug() << remote_ip.toString() << "disconnected"; #endif if (joined) { server->player_count--; server->areas[current_area]->player_count--; arup(ARUPType::PLAYER_COUNT, true); } if (current_char != "") { server->areas[current_area]->characters_taken[current_char] = false; server->updateCharsTaken(server->areas[current_area]); } bool update_locks; for (AreaData* area : server->areas) { area->owners.removeAll(id); area->invited.removeAll(id); if (area->owners.isEmpty() && area->locked != AreaData::FREE) { area->locked = AreaData::FREE; update_locks = true; } } if (update_locks) arup(ARUPType::LOCKED, true); arup(ARUPType::CM, true); } void AOClient::handlePacket(AOPacket packet) { #ifdef NET_DEBUG qDebug() << "Received packet:" << packet.header << ":" << packet.contents << "args length:" << packet.contents.length(); #endif AreaData* area = server->areas[current_area]; PacketInfo info = packets.value(packet.header, {false, 0, &AOClient::pktDefault}); if (!checkAuth(info.acl_mask)) { return; } if (packet.contents.length() < info.minArgs) { #ifdef NET_DEBUG qDebug() << "Invalid packet args length. Minimum is" << info.minArgs << "but only" << packet.contents.length() << "were given."; #endif return; } (this->*(info.action))(area, packet.contents.length(), packet.contents, packet); } void AOClient::changeArea(int new_area) { if (current_area == new_area) { sendServerMessage("You are already in area " + server->area_names[current_area]); return; } if (server->areas[new_area]->locked == AreaData::LockStatus::LOCKED && !server->areas[new_area]->invited.contains(id)) { sendServerMessage("Area " + server->area_names[new_area] + " is locked."); return; } if (current_char != "") { server->areas[current_area]->characters_taken[current_char] = false; server->updateCharsTaken(server->areas[current_area]); } server->areas[new_area]->player_count++; server->areas[current_area]->player_count--; current_area = new_area; arup(ARUPType::PLAYER_COUNT, true); sendEvidenceList(server->areas[new_area]); sendPacket("HP", {"1", QString::number(server->areas[new_area]->def_hp)}); sendPacket("HP", {"2", QString::number(server->areas[new_area]->pro_hp)}); sendPacket("BN", {server->areas[new_area]->background}); if (server->areas[current_area]->characters_taken[current_char]) { server->updateCharsTaken(server->areas[current_area]); current_char = ""; sendPacket("DONE"); } else { server->areas[current_area]->characters_taken[current_char] = true; server->updateCharsTaken(server->areas[current_area]); } for (QTimer* timer : server->areas[current_area]->timers) { int timer_id = server->areas[current_area]->timers.indexOf(timer) + 1; if (timer->isActive()) { sendPacket("TI", {QString::number(timer_id), QString::number(2)}); sendPacket("TI", {QString::number(timer_id), QString::number(0), QString::number(QTime(0,0).msecsTo(QTime(0,0).addMSecs(timer->remainingTime())))}); } else { sendPacket("TI", {QString::number(timer_id), QString::number(3)}); } } sendServerMessage("You moved to area " + server->area_names[current_area]); if (server->areas[current_area]->locked == AreaData::LockStatus::SPECTATABLE) sendServerMessage("Area " + server->area_names[current_area] + " is spectate-only; to chat IC you will need to be invited by the CM."); } void AOClient::changeCharacter(int char_id) { AreaData* area = server->areas[current_area]; if (current_char != "") { area->characters_taken[current_char] = false; } if(char_id > server->characters.length()) return; if (char_id >= 0) { QString char_selected = server->characters[char_id]; bool taken = area->characters_taken.value(char_selected); if (taken || char_selected == "") return; area->characters_taken[char_selected] = true; current_char = char_selected; } else { current_char = ""; } pos = ""; server->updateCharsTaken(area); sendPacket("PV", {QString::number(id), "CID", QString::number(char_id)}); fullArup(); if (server->timer->isActive()) { sendPacket("TI", {"0", "2"}); sendPacket("TI", {"0", "0", QString::number(QTime(0,0).msecsTo(QTime(0,0).addMSecs(server->timer->remainingTime())))}); } else { sendPacket("TI", {"0", "3"}); } for (QTimer* timer : area->timers) { int timer_id = area->timers.indexOf(timer) + 1; if (timer->isActive()) { sendPacket("TI", {QString::number(timer_id), "2"}); sendPacket("TI", {QString::number(timer_id), "0", QString::number(QTime(0,0).msecsTo(QTime(0,0).addMSecs(timer->remainingTime())))}); } else { sendPacket("TI", {QString::number(timer_id), "3"}); } } } void AOClient::changePosition(QString new_pos) { pos = new_pos; sendServerMessage("Position changed to " + pos + "."); sendPacket("SP", {pos}); } void AOClient::handleCommand(QString command, int argc, QStringList argv) { CommandInfo info = commands.value(command, {false, -1, &AOClient::cmdDefault}); if (!checkAuth(info.acl_mask)) { sendServerMessage("You do not have permission to use that command."); return; } if (argc < info.minArgs) { sendServerMessage("Invalid command syntax."); return; } (this->*(info.action))(argc, argv); } void AOClient::arup(ARUPType type, bool broadcast) { QStringList arup_data; arup_data.append(QString::number(type)); for (AreaData* area : server->areas) { switch(type) { case ARUPType::PLAYER_COUNT: { arup_data.append(QString::number(area->player_count)); break; } case ARUPType::STATUS: { QString area_status = QVariant::fromValue(area->status).toString().replace("_", "-"); // LOOKING_FOR_PLAYERS to LOOKING-FOR-PLAYERS arup_data.append(area_status); break; } case ARUPType::CM: { if (area->owners.isEmpty()) arup_data.append("FREE"); else { QStringList area_owners; for (int owner_id : area->owners) { AOClient* owner = server->getClientByID(owner_id); area_owners.append("[" + QString::number(owner->id) + "] " + owner->current_char); } arup_data.append(area_owners.join(", ")); } break; } case ARUPType::LOCKED: { QString lock_status = QVariant::fromValue(area->locked).toString(); arup_data.append(lock_status); break; } default: { return; } } } if (broadcast) server->broadcast(AOPacket("ARUP", arup_data)); else sendPacket("ARUP", arup_data); } void AOClient::fullArup() { arup(ARUPType::PLAYER_COUNT, false); arup(ARUPType::STATUS, false); arup(ARUPType::CM, false); arup(ARUPType::LOCKED, false); } void AOClient::sendPacket(AOPacket packet) { #ifdef NET_DEBUG qDebug() << "Sent packet:" << packet.header << ":" << packet.contents; #endif socket->write(packet.toUtf8()); socket->flush(); } void AOClient::sendPacket(QString header, QStringList contents) { sendPacket(AOPacket(header, contents)); } void AOClient::sendPacket(QString header) { sendPacket(AOPacket(header, {})); } QString AOClient::getHwid() { return hwid; } void AOClient::setHwid(QString p_hwid) { // TODO: add support for longer hwids? // This reduces the (fairly high) chance of // birthday paradox issues arising. However, // typing more than 8 characters might be a // bit cumbersome. hwid = p_hwid; QCryptographicHash hash( QCryptographicHash::Md5); // Don't need security, just // hashing for uniqueness QString concat_ip_id = remote_ip.toString() + p_hwid; hash.addData(concat_ip_id.toUtf8()); ipid = hash.result().toHex().right(8); // Use the last 8 characters (4 bytes) } void AOClient::sendServerMessage(QString message) { sendPacket("CT", {server->getServerName(), message, "1"}); } void AOClient::sendServerMessageArea(QString message) { server->broadcast(AOPacket("CT", {server->getServerName(), message, "1"}), current_area); } void AOClient::sendServerBroadcast(QString message) { server->broadcast(AOPacket("CT", {server->getServerName(), message, "1"})); } bool AOClient::checkAuth(unsigned long long acl_mask) { if (acl_mask != ACLFlags.value("NONE")) { if (acl_mask == ACLFlags.value("CM")) { AreaData* area = server->areas[current_area]; if (area->owners.contains(id)) return true; } else if (!authenticated) { return false; } QSettings settings("config/config.ini", QSettings::IniFormat); settings.beginGroup("Options"); QString auth_type = settings.value("auth", "simple").toString(); if (auth_type == "advanced") { unsigned long long user_acl = server->db_manager->getACL(moderator_name); return (user_acl & acl_mask) != 0; } else if (auth_type == "simple") { return authenticated; } } return true; } QString AOClient::getIpid() { return ipid; } Server* AOClient::getServer() { return server; }; AOClient::~AOClient() { socket->deleteLater(); }