diff --git a/bin/config_sample/config.ini b/bin/config_sample/config.ini index 4f80324..dcbff37 100644 --- a/bin/config_sample/config.ini +++ b/bin/config_sample/config.ini @@ -20,6 +20,7 @@ maximum_statements=10 multiclient_limit=15 maximum_characters=256 message_floodguard=250 +asset_url=Your WebAO asset url here. [Dice] max_value=100 diff --git a/include/aoclient.h b/include/aoclient.h index b474ced..030db74 100644 --- a/include/aoclient.h +++ b/include/aoclient.h @@ -787,6 +787,16 @@ class AOClient : public QObject { */ void cmdLogout(int argc, QStringList argv); + /** + * @brief Changes a moderator's password. + * + * @details If it is called with **one argument**, that argument is the **new password** to change to. + * + * If it is called with **two arguments**, the first argument is the **new password** to change to, + * and the second argument is the **username** of the moderator to change the password of. + */ + void cmdChangePassword(int argc, QStringList argv); + ///@} /** @@ -1005,14 +1015,12 @@ class AOClient : public QObject { * @brief Bans a client from the server, forcibly severing its connection to the server, * and disallowing their return. * - * @details The first argument is the **target's IPID**, the second is the **reason** why the client - * was banned, the third is the **duration**. + * @details The first argument is the **target's IPID**, the second is the **duration**, + * and the third is the **reason** why the client was banned. * - * Both the reason and the duration must be in quotation marks. - * - * The duration can be `"perma"`, meaning a forever ban, otherwise, it must be given in the format of `"YYyWWwDDdHHhMMmSSs"` to + * The duration can be `perma`, meaning a forever ban, otherwise, it must be given in the format of `YYyWWwDDdHHhMMmSSs` to * mean a YY years, WW weeks, DD days, HH hours, MM minutes and SS seconds long ban. Any of these may be left out, for example, - * `"1h30m"` for a 1.5 hour long ban. + * `1h30m` for a 1.5 hour long ban. * * Besides banning, this command kicks all clients having the given IPID, * thus a multiclienting user will have all their clients be kicked from the server. @@ -2034,6 +2042,7 @@ class AOClient : public QObject { {"firstperson", {ACLFlags.value("NONE"), 0, &AOClient::cmdFirstPerson}}, {"updateban", {ACLFlags.value("BAN"), 3, &AOClient::cmdUpdateBan}}, {"update_ban", {ACLFlags.value("BAN"), 3, &AOClient::cmdUpdateBan}}, + {"changepass", {ACLFlags.value("NONE"), 1, &AOClient::cmdChangePassword}}, }; /** diff --git a/include/db_manager.h b/include/db_manager.h index bfd3fe2..02c56e5 100644 --- a/include/db_manager.h +++ b/include/db_manager.h @@ -18,6 +18,8 @@ #ifndef BAN_MANAGER_H #define BAN_MANAGER_H +#define DB_VERSION 1 + #include #include #include @@ -127,6 +129,7 @@ public: QString reason; //!< The reason given for the ban by the moderator who registered it. long long duration; //!< The duration of the ban, in seconds. int id; //!< The unique ID of the ban. + QString moderator; //!< The moderator who issued the ban. }; /** @@ -262,6 +265,17 @@ public: */ bool updateBan(int ban_id, QString field, QVariant updated_info); + /** + * @brief Updates the password of the given user. + * + * @param username The username to change. + * + * @param password The new password to change to. + * + * @return True if the password change was successful. + */ + bool updatePassword(QString username, QString password); + private: /** * @brief The name of the database connection driver. @@ -282,6 +296,25 @@ private: * @brief The backing database that stores user details. */ QSqlDatabase db; + + /** + * @brief The current server DB version. + */ + int db_version; + + /** + * @brief checkVersion Checks the current server DB version. + * + * @return Returns the server DB version. + */ + int checkVersion(); + + /** + * @brief updateDB Updates the server DB to the latest version. + * + * @param current_version The current DB version. + */ + void updateDB(int current_version); }; #endif // BAN_MANAGER_H diff --git a/include/server.h b/include/server.h index 3e610d2..c76417b 100644 --- a/include/server.h +++ b/include/server.h @@ -317,6 +317,10 @@ class Server : public QObject { * @brief The minimum time between IC messages, in milliseconds. */ int message_floodguard; + /** + * @brief URL send to the client during handshake to set the remote repository URL. + */ + QUrl asset_url; public slots: /** diff --git a/src/aoclient.cpp b/src/aoclient.cpp index 3784b4a..a8906a8 100644 --- a/src/aoclient.cpp +++ b/src/aoclient.cpp @@ -265,6 +265,10 @@ void AOClient::sendPacket(AOPacket packet) #ifdef NET_DEBUG qDebug() << "Sent packet:" << packet.header << ":" << packet.contents; #endif + packet.contents.replaceInStrings("#", "") + .replaceInStrings("%", "") + .replaceInStrings("$", "") + .replaceInStrings("&", ""); socket->write(packet.toUtf8()); socket->flush(); } diff --git a/src/commands/authentication.cpp b/src/commands/authentication.cpp index 436608a..4ab5c8a 100644 --- a/src/commands/authentication.cpp +++ b/src/commands/authentication.cpp @@ -232,3 +232,33 @@ void AOClient::cmdLogout(int argc, QStringList argv) moderator_name = ""; sendPacket("AUTH", {"-1"}); // Client: "You were logged out." } + +void AOClient::cmdChangePassword(int argc, QStringList argv) +{ + QString username; + QString password; + if (argc == 1) { + if (moderator_name.isEmpty()) { + sendServerMessage("You are not logged in."); + return; + } + username = moderator_name; + password = argv[0]; + } + else if (argc == 2 && checkAuth(ACLFlags.value("SUPER"))) { + username = argv[0]; + password = argv[1]; + } + else { + sendServerMessage("Invalid command syntax."); + return; + } + + if (server->db_manager->updatePassword(username, password)) { + sendServerMessage("Successfully changed password."); + } + else { + sendServerMessage("There was an error changing the password."); + return; + } +} diff --git a/src/commands/moderation.cpp b/src/commands/moderation.cpp index e027198..4664e9d 100644 --- a/src/commands/moderation.cpp +++ b/src/commands/moderation.cpp @@ -22,38 +22,24 @@ void AOClient::cmdBan(int argc, QStringList argv) { - QString args_str = argv[1]; - if (argc > 2) { - for (int i = 2; i < argc; i++) + QString args_str = argv[2]; + if (argc > 3) { + for (int i = 3; i < argc; i++) args_str += " " + argv[i]; } DBManager::BanInfo ban; - QRegularExpression quoteMatcher("['\"](.+?)[\"']"); - QRegularExpressionMatchIterator matches = quoteMatcher.globalMatch(args_str); - QList unquoted_args; - while (matches.hasNext()) { - QRegularExpressionMatch match = matches.next(); - unquoted_args.append(match.captured(1)); - } - - QString duration = "perma"; - - if (unquoted_args.length() < 1) { - sendServerMessage("Invalid syntax. Usage:\n/ban \"\" \"\""); + if (argc < 3) { + sendServerMessage("Invalid syntax. Usage:\n/ban "); return; } - ban.reason = unquoted_args.at(0); - if (unquoted_args.length() > 1) - duration = unquoted_args.at(1); - long long duration_seconds = 0; - if (duration == "perma") + if (argv[1] == "perma") duration_seconds = -2; else - duration_seconds = parseTime(duration); + duration_seconds = parseTime(argv[1]); if (duration_seconds == -1) { sendServerMessage("Invalid time format. Format example: 1h30m"); @@ -61,16 +47,17 @@ void AOClient::cmdBan(int argc, QStringList argv) } ban.duration = duration_seconds; - ban.ipid = argv[0]; + ban.reason = args_str; ban.time = QDateTime::currentDateTime().toSecsSinceEpoch(); bool ban_logged = false; int kick_counter = 0; - if (argc > 2) { - for (int i = 2; i < argv.length(); i++) { - ban.reason += " " + argv[i]; - } + if (server->auth_type == "advanced") { + ban.moderator = moderator_name; + } + else { + ban.moderator = "moderator"; } for (AOClient* client : server->getClientsByIpid(ban.ipid)) { @@ -81,7 +68,14 @@ void AOClient::cmdBan(int argc, QStringList argv) sendServerMessage("Banned user with ipid " + ban.ipid + " for reason: " + ban.reason); ban_logged = true; } - client->sendPacket("KB", {ban.reason + "\nID: " + QString::number(server->db_manager->getBanID(ban.ip)) + "\nUntil: " + QDateTime::fromSecsSinceEpoch(ban.time).addSecs(ban.duration).toString("dd.MM.yyyy, hh:mm")}); + QString ban_duration; + if (!(ban.duration == -2)) { + ban_duration = QDateTime::fromSecsSinceEpoch(ban.time).addSecs(ban.duration).toString("MM/dd/yyyy, hh:mm"); + } + else { + ban_duration = "The heat death of the universe."; + } + client->sendPacket("KB", {ban.reason + "\nID: " + QString::number(server->db_manager->getBanID(ban.ip)) + "\nUntil: " + ban_duration}); client->socket->close(); kick_counter++; } @@ -185,6 +179,7 @@ void AOClient::cmdBans(int argc, QStringList argv) recent_bans << "Reason for ban: " + ban.reason; recent_bans << "Date of ban: " + QDateTime::fromSecsSinceEpoch(ban.time).toString("MM/dd/yyyy, hh:mm"); recent_bans << "Ban lasts until: " + banned_until; + recent_bans << "Moderator: " + ban.moderator; recent_bans << "-----"; } sendServerMessage(recent_bans.join("\n")); @@ -369,12 +364,14 @@ void AOClient::cmdBanInfo(int argc, QStringList argv) if (ban.duration == -2) banned_until = "The heat death of the universe"; else - banned_until = QDateTime::fromSecsSinceEpoch(ban.time).addSecs(ban.duration).toString("dd.MM.yyyy, hh:mm"); + banned_until = QDateTime::fromSecsSinceEpoch(ban.time).addSecs(ban.duration).toString("MM/dd/yyyy, hh:mm"); + ban_info << "Ban ID: " + QString::number(ban.id); ban_info << "Affected IPID: " + ban.ipid; ban_info << "Affected HDID: " + ban.hdid; ban_info << "Reason for ban: " + ban.reason; - ban_info << "Date of ban: " + QDateTime::fromSecsSinceEpoch(ban.time).toString("dd.MM.yyyy, hh:mm"); + ban_info << "Date of ban: " + QDateTime::fromSecsSinceEpoch(ban.time).toString("MM/dd/yyyy, hh:mm"); ban_info << "Ban lasts until: " + banned_until; + ban_info << "Moderator: " + ban.moderator; ban_info << "-----"; } sendServerMessage(ban_info.join("\n")); diff --git a/src/db_manager.cpp b/src/db_manager.cpp index e6edb36..7a3ad16 100644 --- a/src/db_manager.cpp +++ b/src/db_manager.cpp @@ -24,8 +24,11 @@ DBManager::DBManager() : db.setDatabaseName("config/akashi.db"); if (!db.open()) qCritical() << "Database Error:" << db.lastError(); - QSqlQuery create_ban_table("CREATE TABLE IF NOT EXISTS bans ('ID' INTEGER, 'IPID' TEXT, 'HDID' TEXT, 'IP' TEXT, 'TIME' INTEGER, 'REASON' TEXT, 'DURATION' INTEGER, PRIMARY KEY('ID' AUTOINCREMENT))"); + db_version = checkVersion(); + QSqlQuery create_ban_table("CREATE TABLE IF NOT EXISTS bans ('ID' INTEGER, 'IPID' TEXT, 'HDID' TEXT, 'IP' TEXT, 'TIME' INTEGER, 'REASON' TEXT, 'DURATION' INTEGER, 'MODERATOR' TEXT, PRIMARY KEY('ID' AUTOINCREMENT))"); QSqlQuery create_user_table("CREATE TABLE IF NOT EXISTS users ('ID' INTEGER, 'USERNAME' TEXT, 'SALT' TEXT, 'PASSWORD' TEXT, 'ACL' TEXT, PRIMARY KEY('ID' AUTOINCREMENT))"); + if (db_version != DB_VERSION) + updateDB(db_version); } bool DBManager::isIPBanned(QHostAddress ip) @@ -168,6 +171,7 @@ QList DBManager::getRecentBans() ban.time = static_cast(query.value(4).toULongLong()); ban.reason = query.value(5).toString(); ban.duration = query.value(6).toLongLong(); + ban.moderator = query.value(7).toString(); return_list.append(ban); } std::reverse(return_list.begin(), return_list.end()); @@ -177,13 +181,14 @@ QList DBManager::getRecentBans() void DBManager::addBan(BanInfo ban) { QSqlQuery query; - query.prepare("INSERT INTO BANS(IPID, HDID, IP, TIME, REASON, DURATION) VALUES(?, ?, ?, ?, ?, ?)"); + query.prepare("INSERT INTO BANS(IPID, HDID, IP, TIME, REASON, DURATION, MODERATOR) VALUES(?, ?, ?, ?, ?, ?, ?)"); query.addBindValue(ban.ipid); query.addBindValue(ban.hdid); query.addBindValue(ban.ip.toString()); query.addBindValue(QString::number(ban.time)); query.addBindValue(ban.reason); query.addBindValue(ban.duration); + query.addBindValue(ban.moderator); if (!query.exec()) qDebug() << "SQL Error:" << query.lastError().text(); } @@ -347,15 +352,16 @@ QList DBManager::getBanInfo(QString lookup_type, QString id) query.addBindValue(id); query.setForwardOnly(true); query.exec(); - while (query.next()) { BanInfo ban; - ban.ipid = query.value(0).toString(); - ban.hdid = query.value(1).toString(); - ban.ip = QHostAddress(query.value(2).toString()); - ban.time = static_cast(query.value(3).toULongLong()); - ban.reason = query.value(4).toString(); - ban.duration = query.value(5).toLongLong(); + ban.id = query.value(0).toInt(); + ban.ipid = query.value(1).toString(); + ban.hdid = query.value(2).toString(); + ban.ip = QHostAddress(query.value(3).toString()); + ban.time = static_cast(query.value(4).toULongLong()); + ban.reason = query.value(5).toString(); + ban.duration = query.value(6).toLongLong(); + ban.moderator = query.value(7).toString(); return_list.append(ban); } std::reverse(return_list.begin(), return_list.end()); @@ -383,6 +389,58 @@ bool DBManager::updateBan(int ban_id, QString field, QVariant updated_info) } } +bool DBManager::updatePassword(QString username, QString password) +{ + QString salt; + QSqlQuery salt_check; + salt_check.prepare("SELECT SALT FROM users WHERE USERNAME = ?"); + salt_check.addBindValue(username); + salt_check.exec(); + + if (!salt_check.first()) + return false; + else + salt = salt_check.value(0).toString(); + + QSqlQuery query; + + QString salted_password; + QMessageAuthenticationCode hmac(QCryptographicHash::Sha256); + hmac.setKey(salt.toUtf8()); + hmac.addData(password.toUtf8()); + salted_password = hmac.result().toHex(); + + query.prepare("UPDATE users SET PASSWORD = ? WHERE USERNAME = ?"); + query.addBindValue(salted_password); + query.addBindValue(username); + query.exec(); + return true; +} + +int DBManager::checkVersion() +{ + QSqlQuery query; + query.prepare("PRAGMA user_version"); + query.exec(); + if (query.first()) { + return query.value(0).toInt(); + } + else { + return 0; + } +} + +void DBManager::updateDB(int current_version) +{ + switch (current_version) { + case 0: + QSqlQuery("ALTER TABLE bans ADD COLUMN MODERATOR TEXT"); + case 1: + QSqlQuery ("PRAGMA user_version = " + QString::number(DB_VERSION)); + break; + } +} + DBManager::~DBManager() { db.close(); diff --git a/src/packets.cpp b/src/packets.cpp index 14dfd73..75ae6a5 100644 --- a/src/packets.cpp +++ b/src/packets.cpp @@ -63,6 +63,11 @@ void AOClient::pktSoftwareId(AreaData* area, int argc, QStringList argv, AOPacke sendPacket("PN", {QString::number(server->player_count), server->max_players}); sendPacket("FL", feature_list); + + if (server->asset_url.isValid()) { + QByteArray asset_url = server->asset_url.toEncoded(QUrl::EncodeSpaces); + sendPacket("ASS", {asset_url}); + } } void AOClient::pktBeginLoad(AreaData* area, int argc, QStringList argv, AOPacket packet) diff --git a/src/server.cpp b/src/server.cpp index 21bf840..2afdbe6 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -295,6 +295,9 @@ void Server::loadServerConfig() message_floodguard = config.value("message_floodguard", "250").toInt(&message_floodguard_conversion_success); if (!message_floodguard_conversion_success) message_floodguard = 30; + asset_url = config.value("asset_url","").toString().toUtf8(); + if (!asset_url.isValid()) + asset_url = NULL; config.endGroup(); //Load dice values