////////////////////////////////////////////////////////////////////////////////////// // 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" #include #include "include/akashidefs.h" #include "include/aopacket.h" #include "include/area_data.h" #include "include/config_manager.h" #include "include/db_manager.h" #include "include/music_manager.h" #include "include/server.h" void AOClient::pktDefault(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(argv); #ifdef NET_DEBUG qDebug() << "Unimplemented packet:" << packet.header << packet.contents; #else Q_UNUSED(packet); #endif } void AOClient::pktHardwareId(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(packet); m_hwid = argv[0]; emit server->logConnectionAttempt(m_remote_ip.toString(), m_ipid, m_hwid); auto l_ban = server->getDatabaseManager()->isHDIDBanned(m_hwid); if (l_ban.first) { sendPacket("BD", {l_ban.second + "\nBan ID: " + QString::number(server->getDatabaseManager()->getBanID(m_hwid))}); m_socket->close(); return; } sendPacket("ID", {QString::number(m_id), "akashi", QCoreApplication::applicationVersion()}); } void AOClient::pktSoftwareId(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(packet); // Full feature list as of AO 2.8.5 // The only ones that are critical to ensuring the server works are // "noencryption" and "fastloading" QStringList l_feature_list = { "noencryption", "yellowtext", "prezoom", "flipping", "customobjections", "fastloading", "deskmod", "evidence", "cccc_ic_support", "arup", "casing_alerts", "modcall_reason", "looping_sfx", "additive", "effects", "y_offset", "expanded_desk_mods", "auth_packet"}; m_version.string = argv[1]; QRegularExpression rx("\\b(\\d+)\\.(\\d+)\\.(\\d+)\\b"); // matches X.X.X (e.g. 2.9.0, 2.4.10, etc.) QRegularExpressionMatch l_match = rx.match(m_version.string); if (l_match.hasMatch()) { m_version.release = l_match.captured(1).toInt(); m_version.major = l_match.captured(2).toInt(); m_version.minor = l_match.captured(3).toInt(); } sendPacket("PN", {QString::number(server->getPlayerCount()), QString::number(ConfigManager::maxPlayers()), ConfigManager::serverDescription()}); sendPacket("FL", l_feature_list); if (ConfigManager::assetUrl().isValid()) { QByteArray l_asset_url = ConfigManager::assetUrl().toEncoded(QUrl::EncodeSpaces); sendPacket("ASS", {l_asset_url}); } } void AOClient::pktBeginLoad(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(argv); Q_UNUSED(packet); // Evidence isn't loaded during this part anymore // As a result, we can always send "0" for evidence length // Client only cares about what it gets from LE sendPacket("SI", {QString::number(server->getCharacterCount()), "0", QString::number(server->getAreaCount() + server->getMusicList().length())}); } void AOClient::pktRequestChars(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(argv); Q_UNUSED(packet); sendPacket("SC", server->getCharacters()); } void AOClient::pktRequestMusic(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(argv); Q_UNUSED(packet); sendPacket("SM", server->getAreaNames() + server->getMusicList()); } void AOClient::pktLoadingDone(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(argc); Q_UNUSED(argv); Q_UNUSED(packet); if (m_hwid == "") { // No early connecting! m_socket->close(); return; } if (m_joined) { return; } m_joined = true; server->updateCharsTaken(area); sendEvidenceList(area); sendPacket("HP", {"1", QString::number(area->defHP())}); sendPacket("HP", {"2", QString::number(area->proHP())}); sendPacket("FA", server->getAreaNames()); // Here lies OPPASS, the genius of FanatSors who send the modpass to everyone in plain text. sendPacket("DONE"); sendPacket("BN", {area->background()}); sendServerMessage("=== MOTD ===\r\n" + ConfigManager::motd() + "\r\n============="); fullArup(); // Give client all the area data 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"}); } const QList l_timers = area->timers(); for (QTimer *l_timer : l_timers) { int l_timer_id = area->timers().indexOf(l_timer) + 1; if (l_timer->isActive()) { sendPacket("TI", {QString::number(l_timer_id), "2"}); sendPacket("TI", {QString::number(l_timer_id), "0", QString::number(QTime(0, 0).msecsTo(QTime(0, 0).addMSecs(l_timer->remainingTime())))}); } else { sendPacket("TI", {QString::number(l_timer_id), "3"}); } } emit joined(); area->clientJoinedArea(-1, m_id); arup(ARUPType::PLAYER_COUNT, true); // Tell everyone there is a new player } void AOClient::pktCharPassword(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(packet); m_password = argv[0]; } void AOClient::pktSelectChar(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(packet); bool argument_ok; int l_selected_char_id = argv[1].toInt(&argument_ok); if (!argument_ok) { l_selected_char_id = SPECTATOR_ID; } if (changeCharacter(l_selected_char_id)) m_char_id = l_selected_char_id; if (m_char_id > SPECTATOR_ID) { setSpectator(false); } } void AOClient::pktIcChat(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(argc); Q_UNUSED(argv); if (m_is_muted) { sendServerMessage("You cannot speak while muted."); return; } if (!area->isMessageAllowed() || !server->isMessageAllowed()) { return; } AOPacket validated_packet = validateIcPacket(packet); if (validated_packet.header == "INVALID") return; if (m_pos != "") validated_packet.contents[5] = m_pos; server->broadcast(validated_packet, m_current_area); emit logIC((m_current_char + " " + m_showname), m_ooc_name, m_ipid, server->getAreaById(m_current_area)->name(), m_last_message); area->updateLastICMessage(validated_packet.contents); area->startMessageFloodguard(ConfigManager::messageFloodguard()); server->startMessageFloodguard(ConfigManager::globalMessageFloodguard()); } void AOClient::pktOocChat(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(argc); Q_UNUSED(packet); if (m_is_ooc_muted) { sendServerMessage("You are OOC muted, and cannot speak."); return; } m_ooc_name = dezalgo(argv[0]).replace(QRegExp("\\[|\\]|\\{|\\}|\\#|\\$|\\%|\\&"), ""); // no fucky wucky shit here if (m_ooc_name.isEmpty() || m_ooc_name == ConfigManager::serverName()) // impersonation & empty name protection return; if (m_ooc_name.length() > 30) { sendServerMessage("Your name is too long! Please limit it to under 30 characters."); return; } if (m_is_logging_in) { loginAttempt(argv[1]); return; } QString l_message = dezalgo(argv[1]); if (l_message.length() == 0 || l_message.length() > ConfigManager::maxCharacters()) return; AOPacket final_packet("CT", {m_ooc_name, l_message, "0"}); if (l_message.at(0) == '/') { QStringList l_cmd_argv = l_message.split(" ", akashi::SkipEmptyParts); QString l_command = l_cmd_argv[0].trimmed().toLower(); l_command = l_command.right(l_command.length() - 1); l_cmd_argv.removeFirst(); int l_cmd_argc = l_cmd_argv.length(); handleCommand(l_command, l_cmd_argc, l_cmd_argv); emit logCMD((m_current_char + " " + m_showname), m_ipid, m_ooc_name, l_command, l_cmd_argv, server->getAreaById(m_current_area)->name()); return; } else { server->broadcast(final_packet, m_current_area); } emit logOOC((m_current_char + " " + m_showname), m_ooc_name, m_ipid, area->name(), l_message); } void AOClient::pktPing(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(argv); Q_UNUSED(packet); // Why does this packet exist // At least Crystal made it useful // It is now used for ping measurement sendPacket("CHECK"); } void AOClient::pktChangeMusic(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(packet); // Due to historical reasons, this // packet has two functions: // Change area, and set music. // First, we check if the provided // argument is a valid song QString l_argument = argv[0]; if (server->getMusicList().contains(l_argument) || m_music_manager->isCustom(m_current_area, l_argument) || l_argument == "~stop.mp3") { // ~stop.mp3 is a dummy track used by 2.9+ // We have a song here if (m_is_spectator) { sendServerMessage("Spectator are blocked from changing the music."); return; } if (m_is_dj_blocked) { sendServerMessage("You are blocked from changing the music."); return; } if (!area->isMusicAllowed() && !checkPermission(ACLRole::CM)) { sendServerMessage("Music is disabled in this area."); return; } QString l_effects; if (argc >= 4) l_effects = argv[3]; else l_effects = "0"; QString l_final_song; // As categories can be used to stop music we need to check if it has a dot for the extension. If not, we assume its a category. if (!l_argument.contains(".")) l_final_song = "~stop.mp3"; else l_final_song = l_argument; // Jukebox intercepts the direct playing of messages. if (area->isjukeboxEnabled()) { QString l_jukebox_reply = area->addJukeboxSong(l_final_song); sendServerMessage(l_jukebox_reply); return; } if (l_final_song != "~stop.mp3") { // We might have an aliased song. We check for its real songname and send it to the clients. QPair l_song = m_music_manager->songInformation(l_final_song, m_current_area); l_final_song = l_song.first; } AOPacket l_music_change("MC", {l_final_song, argv[1], m_showname, "1", "0", l_effects}); server->broadcast(l_music_change, m_current_area); // Since we can't ensure a user has their showname set, we check if its empty to prevent //"played by ." in /currentmusic. if (m_showname.isEmpty()) { area->changeMusic(m_current_char, l_final_song); return; } area->changeMusic(m_showname, l_final_song); return; } for (int i = 0; i < server->getAreaCount(); i++) { QString l_area = server->getAreaName(i); if (l_area == l_argument) { changeArea(i); break; } } } void AOClient::pktWtCe(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(argc); Q_UNUSED(argv); if (m_is_wtce_blocked) { sendServerMessage("You are blocked from using the judge controls."); return; } if (QDateTime::currentDateTime().toSecsSinceEpoch() - m_last_wtce_time <= 5) return; m_last_wtce_time = QDateTime::currentDateTime().toSecsSinceEpoch(); server->broadcast(packet, m_current_area); updateJudgeLog(area, this, "WT/CE"); } void AOClient::pktHpBar(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(argc); Q_UNUSED(packet); if (m_is_wtce_blocked) { sendServerMessage("You are blocked from using the judge controls."); return; } int l_newValue = argv.at(1).toInt(); if (argv[0] == "1") { area->changeHP(AreaData::Side::DEFENCE, l_newValue); } else if (argv[0] == "2") { area->changeHP(AreaData::Side::PROSECUTOR, l_newValue); } server->broadcast(AOPacket("HP", {"1", QString::number(area->defHP())}), area->index()); server->broadcast(AOPacket("HP", {"2", QString::number(area->proHP())}), area->index()); updateJudgeLog(area, this, "updated the penalties"); } void AOClient::pktWebSocketIp(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(packet); // Special packet to set remote IP from the webao proxy // Only valid if from a local ip if (m_remote_ip.isLoopback()) { #ifdef NET_DEBUG qDebug() << "ws ip set to" << argv[0]; #endif m_remote_ip = QHostAddress(argv[0]); QHostAddress l_remote_ip = m_remote_ip; if (l_remote_ip.protocol() == QAbstractSocket::IPv6Protocol) { l_remote_ip = server->parseToIPv4(l_remote_ip); } if (server->isIPBanned(l_remote_ip)) { QString l_reason = "Your IP has been banned by a moderator."; AOPacket l_ban_reason("BD", {l_reason}); m_socket->write(l_ban_reason.toUtf8()); m_socket->close(); return; } calculateIpid(); auto l_ban = server->getDatabaseManager()->isIPBanned(m_ipid); if (l_ban.first) { sendPacket("BD", {l_ban.second}); m_socket->close(); return; } int l_multiclient_count = 0; const QVector l_clients = server->getClients(); for (AOClient *l_joined_client : l_clients) { if (m_remote_ip.isEqual(l_joined_client->m_remote_ip)) l_multiclient_count++; } if (l_multiclient_count > ConfigManager::multiClientLimit()) { m_socket->close(); return; } } } void AOClient::pktModCall(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(argc); Q_UNUSED(argv); QString l_name = m_ooc_name; if (m_ooc_name.isEmpty()) l_name = m_current_char; QString l_areaName = area->name(); QString l_modcallNotice = "!!!MODCALL!!!\nArea: " + l_areaName + "\nCaller: " + l_name + "\n"; if (!packet.contents[0].isEmpty()) l_modcallNotice.append("Reason: " + packet.contents[0]); else l_modcallNotice.append("No reason given."); const QVector l_clients = server->getClients(); for (AOClient *l_client : l_clients) { if (l_client->m_authenticated) l_client->sendPacket(AOPacket("ZZ", {l_modcallNotice})); } emit logModcall((m_current_char + " " + m_showname), m_ipid, m_ooc_name, server->getAreaById(m_current_area)->name()); if (ConfigManager::discordModcallWebhookEnabled()) { QString l_name = m_ooc_name; if (m_ooc_name.isEmpty()) l_name = m_current_char; QString l_areaName = area->name(); emit server->modcallWebhookRequest(l_name, l_areaName, packet.contents[0], server->getAreaBuffer(l_areaName)); } } void AOClient::pktAddEvidence(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(argc); Q_UNUSED(packet); if (!checkEvidenceAccess(area)) return; AreaData::Evidence l_evi = {argv[0], argv[1], argv[2]}; area->appendEvidence(l_evi); sendEvidenceList(area); } void AOClient::pktRemoveEvidence(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(argc); Q_UNUSED(packet); if (!checkEvidenceAccess(area)) return; bool is_int = false; int l_idx = argv[0].toInt(&is_int); if (is_int && l_idx < area->evidence().size() && l_idx >= 0) { area->deleteEvidence(l_idx); } sendEvidenceList(area); } void AOClient::pktEditEvidence(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(argc); Q_UNUSED(packet); if (!checkEvidenceAccess(area)) return; bool is_int = false; int l_idx = argv[0].toInt(&is_int); AreaData::Evidence l_evi = {argv[1], argv[2], argv[3]}; if (is_int && l_idx < area->evidence().size() && l_idx >= 0) { area->replaceEvidence(l_idx, l_evi); } sendEvidenceList(area); } void AOClient::pktSetCase(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(packet); QList l_prefs_list; for (int i = 2; i <= 6; i++) { bool is_int = false; bool pref = argv[i].toInt(&is_int); if (!is_int) return; l_prefs_list.append(pref); } m_casing_preferences = l_prefs_list; } void AOClient::pktAnnounceCase(AreaData *area, int argc, QStringList argv, AOPacket packet) { Q_UNUSED(area); Q_UNUSED(argc); Q_UNUSED(packet); QString l_case_title = argv[0]; QStringList l_needed_roles; QList l_needs_list; for (int i = 1; i <= 5; i++) { bool is_int = false; bool need = argv[i].toInt(&is_int); if (!is_int) return; l_needs_list.append(need); } QStringList l_roles = {"defense attorney", "prosecutor", "judge", "jurors", "stenographer"}; for (int i = 0; i < 5; i++) { if (l_needs_list[i]) l_needed_roles.append(l_roles[i]); } if (l_needed_roles.isEmpty()) return; QString l_message = "=== Case Announcement ===\r\n" + (m_ooc_name == "" ? m_current_char : m_ooc_name) + " needs " + l_needed_roles.join(", ") + " for " + (l_case_title == "" ? "a case" : l_case_title) + "!"; QList l_clients_to_alert; // here lies morton, RIP #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QSet l_needs_set(l_needs_list.begin(), l_needs_list.end()); #else QSet l_needs_set = l_needs_list.toSet(); #endif const QVector l_clients = server->getClients(); for (AOClient *l_client : l_clients) { #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QSet l_matches(l_client->m_casing_preferences.begin(), l_client->m_casing_preferences.end()); l_matches.intersect(l_needs_set); #else QSet l_matches = l_client->m_casing_preferences.toSet().intersect(l_needs_set); #endif if (!l_matches.isEmpty() && !l_clients_to_alert.contains(l_client)) l_clients_to_alert.append(l_client); } for (AOClient *l_client : l_clients_to_alert) { l_client->sendPacket(AOPacket("CASEA", {l_message, argv[1], argv[2], argv[3], argv[4], argv[5], "1"})); // you may be thinking, "hey wait a minute the network protocol documentation doesn't mention that last argument!" // if you are in fact thinking that, you are correct! it is not in the documentation! // however for some inscrutable reason Attorney Online 2 will outright reject a CASEA packet that does not have // at least 7 arguments despite only using the first 6. Cera, i kneel. you have truly broken me. } } void AOClient::sendEvidenceList(AreaData *area) { const QVector l_clients = server->getClients(); for (AOClient *l_client : l_clients) { if (l_client->m_current_area == m_current_area) l_client->updateEvidenceList(area); } } void AOClient::updateEvidenceList(AreaData *area) { QStringList l_evidence_list; QString l_evidence_format("%1&%2&%3"); const QList l_area_evidence = area->evidence(); for (const AreaData::Evidence &evidence : l_area_evidence) { if (!checkPermission(ACLRole::CM) && area->eviMod() == AreaData::EvidenceMod::HIDDEN_CM) { QRegularExpression l_regex(""); QRegularExpressionMatch l_match = l_regex.match(evidence.description); if (l_match.hasMatch()) { QStringList owners = l_match.captured(1).split(","); if (!owners.contains("all", Qt::CaseSensitivity::CaseInsensitive) && !owners.contains(m_pos, Qt::CaseSensitivity::CaseInsensitive)) { continue; } } // no match = show it to all } l_evidence_list.append(l_evidence_format.arg(evidence.name, evidence.description, evidence.image)); } sendPacket(AOPacket("LE", l_evidence_list)); } AOPacket AOClient::validateIcPacket(AOPacket packet) { // Welcome to the super cursed server-side IC chat validation hell // I wanted to use enums or #defines here to make the // indicies of the args arrays more readable. But, // in typical AO fasion, the indicies for the incoming // and outgoing packets are different. Just RTFM. // This packet can be sent with a minimum required args of 15. // 2.6+ extensions raise this to 19, and 2.8 further raises this to 26. AOPacket l_invalid("INVALID", {}); QStringList l_args; if (m_current_char == "" || !m_joined) // Spectators cannot use IC return l_invalid; AreaData *area = server->getAreaById(m_current_area); if (area->lockStatus() == AreaData::LockStatus::SPECTATABLE && !area->invited().contains(m_id) && !checkPermission(ACLRole::BYPASS_LOCKS)) // Non-invited players cannot speak in spectatable areas return l_invalid; QList l_incoming_args; for (const QString &l_arg : qAsConst(packet.contents)) { l_incoming_args.append(QVariant(l_arg)); } // desk modifier QStringList allowed_desk_mods; allowed_desk_mods << "chat" << "0" << "1" << "2" << "3" << "4" << "5"; if (allowed_desk_mods.contains(l_incoming_args[0].toString())) { l_args.append(l_incoming_args[0].toString()); } else return l_invalid; // preanim l_args.append(l_incoming_args[1].toString()); // char name if (m_current_char.toLower() != l_incoming_args[2].toString().toLower()) { // Selected char is different from supplied folder name // This means the user is INI-swapped if (!area->iniswapAllowed()) { if (!server->getCharacters().contains(l_incoming_args[2].toString(), Qt::CaseInsensitive)) return l_invalid; } qDebug() << "INI swap detected from " << getIpid(); } m_current_iniswap = l_incoming_args[2].toString(); l_args.append(l_incoming_args[2].toString()); // emote m_emote = l_incoming_args[3].toString(); if (m_first_person) m_emote = ""; l_args.append(m_emote); // message text if (l_incoming_args[4].toString().size() > ConfigManager::maxCharacters()) return l_invalid; QString l_incoming_msg = dezalgo(l_incoming_args[4].toString().trimmed()); if (!area->lastICMessage().isEmpty() && l_incoming_msg == area->lastICMessage()[4] && l_incoming_msg != "") return l_invalid; if (l_incoming_msg == "" && area->blankpostingAllowed() == false) { sendServerMessage("Blankposting has been forbidden in this area."); return l_invalid; } if (m_is_gimped) { QString l_gimp_message = ConfigManager::gimpList().at((genRand(1, ConfigManager::gimpList().size() - 1))); l_incoming_msg = l_gimp_message; } if (m_is_shaken) { QStringList l_parts = l_incoming_msg.split(" "); std::random_shuffle(l_parts.begin(), l_parts.end()); l_incoming_msg = l_parts.join(" "); } if (m_is_disemvoweled) { QString l_disemvoweled_message = l_incoming_msg.remove(QRegExp("[AEIOUaeiou]")); l_incoming_msg = l_disemvoweled_message; } m_last_message = l_incoming_msg; l_args.append(l_incoming_msg); // side // this is validated clientside so w/e l_args.append(l_incoming_args[5].toString()); if (m_pos != l_incoming_args[5].toString()) { m_pos = l_incoming_args[5].toString(); updateEvidenceList(server->getAreaById(m_current_area)); } // sfx name l_args.append(l_incoming_args[6].toString()); // emote modifier // Now, gather round, y'all. Here is a story that is truly a microcosm of the AO dev experience. // If this value is a 4, it will crash the client. Why? Who knows, but it does. // Now here is the kicker: in certain versions, the client would incorrectly send a 4 here // For a long time, by configuring the client to do a zoom with a preanim, it would send 4 // This would crash everyone else's client, and the feature had to be disabled // But, for some reason, nobody traced the cause of this issue for many many years. // The serverside fix is needed to ensure invalid values are not sent, because the client sucks int emote_mod = l_incoming_args[7].toInt(); if (emote_mod == 4) emote_mod = 6; if (emote_mod != 0 && emote_mod != 1 && emote_mod != 2 && emote_mod != 5 && emote_mod != 6) return l_invalid; l_args.append(QString::number(emote_mod)); // char id if (l_incoming_args[8].toInt() != m_char_id) return l_invalid; l_args.append(l_incoming_args[8].toString()); // sfx delay l_args.append(l_incoming_args[9].toString()); // objection modifier if (l_incoming_args[10].toString().contains("4")) { // custom shout includes text metadata l_args.append(l_incoming_args[10].toString()); } else { int l_obj_mod = l_incoming_args[10].toInt(); if (l_obj_mod != 0 && l_obj_mod != 1 && l_obj_mod != 2 && l_obj_mod != 3) return l_invalid; l_args.append(QString::number(l_obj_mod)); } // evidence int evi_idx = l_incoming_args[11].toInt(); if (evi_idx > area->evidence().length()) return l_invalid; l_args.append(QString::number(evi_idx)); // flipping int l_flip = l_incoming_args[12].toInt(); if (l_flip != 0 && l_flip != 1) return l_invalid; m_flipping = QString::number(l_flip); l_args.append(m_flipping); // realization int realization = l_incoming_args[13].toInt(); if (realization != 0 && realization != 1) return l_invalid; l_args.append(QString::number(realization)); // text color int text_color = l_incoming_args[14].toInt(); if (text_color < 0 || text_color > 11) return l_invalid; l_args.append(QString::number(text_color)); // 2.6 packet extensions if (l_incoming_args.length() >= 19) { // showname QString l_incoming_showname = dezalgo(l_incoming_args[15].toString().trimmed()); if (!(l_incoming_showname == m_current_char || l_incoming_showname.isEmpty()) && !area->shownameAllowed()) { sendServerMessage("Shownames are not allowed in this area!"); return l_invalid; } if (l_incoming_showname.length() > 30) { sendServerMessage("Your showname is too long! Please limit it to under 30 characters"); return l_invalid; } // if the raw input is not empty but the trimmed input is, use a single space if (l_incoming_showname.isEmpty() && !l_incoming_args[15].toString().isEmpty()) l_incoming_showname = " "; l_args.append(l_incoming_showname); m_showname = l_incoming_showname; // other char id // things get a bit hairy here // don't ask me how this works, because i don't know either QStringList l_pair_data = l_incoming_args[16].toString().split("^"); m_pairing_with = l_pair_data[0].toInt(); QString l_front_back = ""; if (l_pair_data.length() > 1) l_front_back = "^" + l_pair_data[1]; int l_other_charid = m_pairing_with; bool l_pairing = false; QString l_other_name = "0"; QString l_other_emote = "0"; QString l_other_offset = "0"; QString l_other_flip = "0"; for (int l_client_id : area->joinedIDs()) { AOClient* l_client = server->getClientByID(l_client_id); if (l_client->m_pairing_with == m_char_id && l_other_charid != m_char_id && l_client->m_char_id == m_pairing_with && l_client->m_pos == m_pos) { l_other_name = l_client->m_current_iniswap; l_other_emote = l_client->m_emote; l_other_offset = l_client->m_offset; l_other_flip = l_client->m_flipping; l_pairing = true; } } if (!l_pairing) { l_other_charid = -1; l_front_back = ""; } l_args.append(QString::number(l_other_charid) + l_front_back); l_args.append(l_other_name); l_args.append(l_other_emote); // self offset m_offset = l_incoming_args[17].toString(); // versions 2.6-2.8 cannot validate y-offset so we send them just the x-offset if ((m_version.release == 2) && (m_version.major == 6 || m_version.major == 7 || m_version.major == 8)) { QString l_x_offset = m_offset.split("&")[0]; l_args.append(l_x_offset); QString l_other_x_offset = l_other_offset.split("&")[0]; l_args.append(l_other_x_offset); } else { l_args.append(m_offset); l_args.append(l_other_offset); } l_args.append(l_other_flip); // immediate text processing int l_immediate = l_incoming_args[18].toInt(); if (area->forceImmediate()) { if (l_args[7] == "1" || l_args[7] == "2") { l_args[7] = "0"; l_immediate = 1; } else if (l_args[7] == "6") { l_args[7] = "5"; l_immediate = 1; } } if (l_immediate != 1 && l_immediate != 0) return l_invalid; l_args.append(QString::number(l_immediate)); } // 2.8 packet extensions if (l_incoming_args.length() >= 26) { // sfx looping int l_sfx_loop = l_incoming_args[19].toInt(); if (l_sfx_loop != 0 && l_sfx_loop != 1) return l_invalid; l_args.append(QString::number(l_sfx_loop)); // screenshake int l_screenshake = l_incoming_args[20].toInt(); if (l_screenshake != 0 && l_screenshake != 1) return l_invalid; l_args.append(QString::number(l_screenshake)); // frames shake l_args.append(l_incoming_args[21].toString()); // frames realization l_args.append(l_incoming_args[22].toString()); // frames sfx l_args.append(l_incoming_args[23].toString()); // additive int l_additive = l_incoming_args[24].toInt(); if (l_additive != 0 && l_additive != 1) return l_invalid; else if (area->lastICMessage().isEmpty()) { l_additive = 0; } else if (!(m_char_id == area->lastICMessage()[8].toInt())) { l_additive = 0; } else if (l_additive == 1) { l_args[4].insert(0, " "); } l_args.append(QString::number(l_additive)); // effect l_args.append(l_incoming_args[25].toString()); } // Testimony playback if (area->testimonyRecording() == AreaData::TestimonyRecording::RECORDING || area->testimonyRecording() == AreaData::TestimonyRecording::ADD) { if (l_args[5] != "wit") return AOPacket("MS", l_args); if (area->statement() == -1) { l_args[4] = "~~\\n-- " + l_args[4] + " --"; l_args[14] = "3"; server->broadcast(AOPacket("RT", {"testimony1"}), m_current_area); } addStatement(l_args); } else if (area->testimonyRecording() == AreaData::TestimonyRecording::UPDATE) { l_args = updateStatement(l_args); } else if (area->testimonyRecording() == AreaData::TestimonyRecording::PLAYBACK) { AreaData::TestimonyProgress l_progress; if (l_args[4] == ">") { m_pos = "wit"; auto l_statement = area->jumpToStatement(area->statement() + 1); l_args = l_statement.first; l_progress = l_statement.second; if (l_progress == AreaData::TestimonyProgress::LOOPED) { sendServerMessageArea("Last statement reached. Looping to first statement."); } } if (l_args[4] == "<") { m_pos = "wit"; auto l_statement = area->jumpToStatement(area->statement() - 1); l_args = l_statement.first; l_progress = l_statement.second; if (l_progress == AreaData::TestimonyProgress::STAYED_AT_FIRST) { sendServerMessage("First statement reached."); } } QString l_decoded_message = decodeMessage(l_args[4]); // Get rid of that pesky encoding first. QRegularExpression jump("(?>)(?[0,1,2,3,4,5,6,7,8,9]+)"); QRegularExpressionMatch match = jump.match(l_decoded_message); if (match.hasMatch()) { m_pos = "wit"; auto l_statement = area->jumpToStatement(match.captured("int").toInt()); l_args = l_statement.first; l_progress = l_statement.second; switch (l_progress) { case AreaData::TestimonyProgress::LOOPED: { sendServerMessageArea("Last statement reached. Looping to first statement."); break; } case AreaData::TestimonyProgress::STAYED_AT_FIRST: { sendServerMessage("First statement reached."); Q_FALLTHROUGH(); } case AreaData::TestimonyProgress::OK: default: // No need to handle. break; } } } return AOPacket("MS", l_args); } QString AOClient::dezalgo(QString p_text) { QRegularExpression rxp("([̴̵̶̷̸̡̢̧̨̛̖̗̘̙̜̝̞̟̠̣̤̥̦̩̪̫̬̭̮̯̰̱̲̳̹̺̻̼͇͈͉͍͎̀́̂̃̄̅̆̇̈̉̊̋̌̍̎̏̐̑̒̓̔̽̾̿̀́͂̓̈́͆͊͋͌̕̚ͅ͏͓͔͕͖͙͚͐͑͒͗͛ͣͤͥͦͧͨͩͪͫͬͭͮͯ͘͜͟͢͝͞͠͡])"); QString filtered = p_text.replace(rxp, ""); return filtered; } bool AOClient::checkEvidenceAccess(AreaData *area) { switch (area->eviMod()) { case AreaData::EvidenceMod::FFA: return true; case AreaData::EvidenceMod::CM: case AreaData::EvidenceMod::HIDDEN_CM: return checkPermission(ACLRole::CM); case AreaData::EvidenceMod::MOD: return m_authenticated; default: return false; } } void AOClient::updateJudgeLog(AreaData *area, AOClient *client, QString action) { QString l_timestamp = QTime::currentTime().toString("hh:mm:ss"); QString l_uid = QString::number(client->m_id); QString l_char_name = client->m_current_char; QString l_ipid = client->getIpid(); QString l_message = action; QString l_logmessage = QString("[%1]: [%2] %3 (%4) %5").arg(l_timestamp, l_uid, l_char_name, l_ipid, l_message); area->appendJudgelog(l_logmessage); } QString AOClient::decodeMessage(QString incoming_message) { QString decoded_message = incoming_message.replace("", "#") .replace("", "%") .replace("", "$") .replace("", "&"); return decoded_message; } void AOClient::loginAttempt(QString message) { switch (ConfigManager::authType()) { case DataTypes::AuthType::SIMPLE: if (message == ConfigManager::modpass()) { sendPacket("AUTH", {"1"}); // Client: "You were granted the Disable Modcalls button." sendServerMessage("Logged in as a moderator."); // pre-2.9.1 clients are hardcoded to display the mod UI when this string is sent in OOC m_authenticated = true; m_acl_role_id = ACLRolesHandler::SUPER_ID; } else { sendPacket("AUTH", {"0"}); // Client: "Login unsuccessful." sendServerMessage("Incorrect password."); } emit logLogin((m_current_char + " " + m_showname), m_ooc_name, "Moderator", m_ipid, server->getAreaById(m_current_area)->name(), m_authenticated); break; case DataTypes::AuthType::ADVANCED: QStringList l_login = message.split(" "); if (l_login.size() < 2) { sendServerMessage("You must specify a username and a password"); sendServerMessage("Exiting login prompt."); m_is_logging_in = false; return; } QString username = l_login[0]; QString password = l_login[1]; if (server->getDatabaseManager()->authenticate(username, password)) { m_authenticated = true; m_acl_role_id = server->getDatabaseManager()->getACL(username); m_moderator_name = username; sendPacket("AUTH", {"1"}); // Client: "You were granted the Disable Modcalls button." if (m_version.release <= 2 && m_version.major <= 9 && m_version.minor <= 0) sendServerMessage("Logged in as a moderator."); // pre-2.9.1 clients are hardcoded to display the mod UI when this string is sent in OOC sendServerMessage("Welcome, " + username); } else { sendPacket("AUTH", {"0"}); // Client: "Login unsuccessful." sendServerMessage("Incorrect password."); } emit logLogin((m_current_char + " " + m_showname), m_ooc_name, username, m_ipid, server->getAreaById(m_current_area)->name(), m_authenticated); break; } sendServerMessage("Exiting login prompt."); m_is_logging_in = false; return; }