From d7bf5ebb589404cf580434cc5e8e490bdbce881c Mon Sep 17 00:00:00 2001 From: Aleos Date: Fri, 14 Oct 2022 10:54:19 -0400 Subject: [PATCH] Adds support for macro detection (#7315) * Adds the official client macro detection system. * Includes the ability to load imagery at server boot. * See doc/captcha_db.txt for more information! Thanks to @Asheraf and @Lemongrass3110! Co-authored-by: Lemongrass3110 --- conf/battle/client.conf | 10 + conf/groups.yml | 2 + db/captcha_db.yml | 37 ++++ db/import-tmpl/captcha_db.yml | 33 ++++ doc/captcha_db.txt | 44 +++++ doc/permissions.txt | 12 ++ src/map/atcommand.cpp | 2 +- src/map/battle.cpp | 2 + src/map/battle.hpp | 2 + src/map/clif.cpp | 267 +++++++++++++++++++++++++ src/map/clif.hpp | 23 +++ src/map/clif_packetdb.hpp | 14 ++ src/map/map-server.vcxproj | 1 + src/map/map.cpp | 1 + src/map/packets_struct.hpp | 6 +- src/map/party.cpp | 2 +- src/map/pc.cpp | 353 ++++++++++++++++++++++++++++++++++ src/map/pc.hpp | 73 +++++++ src/map/pc_groups.hpp | 4 + 19 files changed, 883 insertions(+), 5 deletions(-) create mode 100644 db/captcha_db.yml create mode 100644 db/import-tmpl/captcha_db.yml create mode 100644 doc/captcha_db.txt diff --git a/conf/battle/client.conf b/conf/battle/client.conf index 15c22720b9..5381b0489e 100644 --- a/conf/battle/client.conf +++ b/conf/battle/client.conf @@ -148,3 +148,13 @@ show_skill_scale: yes // Note: Enabling this is known to cause problems on clients that make use of REST API calls. // Official: no drop_connection_on_quit: no + +// Macro Detector retries +// Number of times someone can fail the macro detection before being banned. +// Official: 3 (minimum: 1) +macro_detection_retry: 3 + +// Macro Detector timeout +// Amount of time in milliseconds before the macro detection will fail and the user will be banned. +// Official: 60000 +macro_detection_timeout: 60000 diff --git a/conf/groups.yml b/conf/groups.yml index 542513213d..d61a6d6cc0 100644 --- a/conf/groups.yml +++ b/conf/groups.yml @@ -216,6 +216,7 @@ Body: hack_info: true any_warp: true view_hpmeter: true + macro_detect: true - Id: 99 Name: Admin Level: 99 @@ -238,6 +239,7 @@ Body: item_unconditional: false bypass_stat_onclone: true bypass_max_stat: true + macro_register: true #all_permission: true Footer: diff --git a/db/captcha_db.yml b/db/captcha_db.yml new file mode 100644 index 0000000000..fc20d2c6cf --- /dev/null +++ b/db/captcha_db.yml @@ -0,0 +1,37 @@ +# This file is a part of rAthena. +# Copyright(C) 2022 rAthena Development Team +# https://rathena.org - https://github.com/rathena +# +# This program 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. +# +# 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 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 . +# +########################################################################### +# Captcha Database Table +########################################################################### +# +# Captcha Database Settings +# +########################################################################### +# - Id Index value. +# Filename Name of the BMP image file (with location). +# Answer Correct answer for the captcha (case-sensitive). +# Bonus Bonus Script ran on success. (Default: Level 10 Blessing and Increase Agility) +########################################################################### + +Header: + Type: CAPTCHA_DB + Version: 1 + +Footer: + Imports: + - Path: db/import/captcha_db.yml diff --git a/db/import-tmpl/captcha_db.yml b/db/import-tmpl/captcha_db.yml new file mode 100644 index 0000000000..d96c892937 --- /dev/null +++ b/db/import-tmpl/captcha_db.yml @@ -0,0 +1,33 @@ +# This file is a part of rAthena. +# Copyright(C) 2022 rAthena Development Team +# https://rathena.org - https://github.com/rathena +# +# This program 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. +# +# 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 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 . +# +########################################################################### +# Captcha Database Table +########################################################################### +# +# Captcha Database Settings +# +########################################################################### +# - Id Index value. +# Filename Name of the BMP image file (with location). +# Answer Correct answer for the captcha (case-sensitive). +# Bonus Bonus Script ran on success. (Default: Level 10 Blessing and Increase Agility) +########################################################################### + +Header: + Type: CAPTCHA_DB + Version: 1 diff --git a/doc/captcha_db.txt b/doc/captcha_db.txt new file mode 100644 index 0000000000..0fd6dd76e2 --- /dev/null +++ b/doc/captcha_db.txt @@ -0,0 +1,44 @@ +//===== rAthena Documentation ================================ +//= Captcha Database Structure +//===== By: ================================================== +//= rAthena Dev Team +//===== Last Updated: ======================================== +//= 20220920 +//===== Description: ========================================= +//= Explanation of the captcha_db.yml file and structure. +//============================================================ + +--------------------------------------- + +Id: Unique ID. + +--------------------------------------- + +Filename: Name of the BMP image file (with location). + The path of the file can be different for each captcha image, but it's best practice to keep them in the same directory. + +Example: + Filename: db/import/captcha/rathena.bmp + +--------------------------------------- + +Answer: Correct answer for the captcha (case-sensitive). + +--------------------------------------- + +Bonus: NPC script that is ran when a captcha is successfully answered. Accepts all forms of script constants, variables, as well as the + unique player variable @captcha_retries. This variable can be used within the Bonus script to get the remaining retries a player + has. Coupled with the script command 'getbattleflag()' this could be used to assign different bonuses based on success rate. + +Example: + # Give level 10 Blessing for 20 minutes with no failures, else give for 30 seconds. + Bonus: > + if (@captcha_retries == getbattleflag("macro_detection_retry")) { + # Player solved it on first try + specialeffect2 EF_BLESSING; + sc_start SC_BLESSING,1200000,10; + } else { + # Player needed more than one try + specialeffect2 EF_BLESSING; + sc_start SC_BLESSING,30000,10; + } diff --git a/doc/permissions.txt b/doc/permissions.txt index 63b58141a7..e79b05a0a9 100644 --- a/doc/permissions.txt +++ b/doc/permissions.txt @@ -212,3 +212,15 @@ Allow to bypass the maximum stat parameter (at conf/player.conf) to maximum value 32,767. --------------------------------------- + +*macro_detect + +Allows player to use the client command /macro_detector. + +--------------------------------------- + +*macro_register + +Allows player to use the client commands /maco_register (used to add new captcha) and /macro_preview (used to preview captcha by ID). + +--------------------------------------- diff --git a/src/map/atcommand.cpp b/src/map/atcommand.cpp index 0c82ef87dd..79e299d4e7 100644 --- a/src/map/atcommand.cpp +++ b/src/map/atcommand.cpp @@ -3268,7 +3268,7 @@ ACMD_FUNC(recall) { if ( pc_get_group_level(sd) < pc_get_group_level(pl_sd) ) { - clif_displaymessage(fd, msg_txt(sd,81)); // Your GM level doesn't authorize you to preform this action on the specified player. + clif_displaymessage(fd, msg_txt(sd,81)); // Your GM level doesn't authorize you to perform this action on the specified player. return -1; } diff --git a/src/map/battle.cpp b/src/map/battle.cpp index d91bbac2f5..d803b71c5d 100644 --- a/src/map/battle.cpp +++ b/src/map/battle.cpp @@ -10270,6 +10270,8 @@ static const struct _battle_data { { "feature.barter", &battle_config.feature_barter, 1, 0, 1, }, { "feature.barter_extended", &battle_config.feature_barter_extended, 1, 0, 1, }, { "break_mob_equip", &battle_config.break_mob_equip, 0, 0, 1, }, + { "macro_detection_retry", &battle_config.macro_detection_retry, 3, 1, INT_MAX, }, + { "macro_detection_timeout", &battle_config.macro_detection_timeout, 60000, 0, INT_MAX, }, #include "../custom/battle_config_init.inc" }; diff --git a/src/map/battle.hpp b/src/map/battle.hpp index 6ee129db28..78b54f4fe2 100644 --- a/src/map/battle.hpp +++ b/src/map/battle.hpp @@ -711,6 +711,8 @@ struct Battle_Config int feature_barter; int feature_barter_extended; int break_mob_equip; + int macro_detection_retry; + int macro_detection_timeout; #include "../custom/battle_config_struct.inc" }; diff --git a/src/map/clif.cpp b/src/map/clif.cpp index 3632b92325..def2f29686 100644 --- a/src/map/clif.cpp +++ b/src/map/clif.cpp @@ -21552,6 +21552,14 @@ void clif_parse_open_ui( int fd, struct map_session_data* sd ){ clif_msg_color( sd, MSG_ATTENDANCE_DISABLED, color_table[COLOR_RED] ); } break; +#if PACKETVER >= 20160316 + case IN_UI_MACRO_REGISTER: + clif_ui_open(*sd, OUT_UI_CAPTCHA, 0); + break; + case IN_UI_MACRO_DETECTOR: + clif_ui_open(*sd, OUT_UI_MACRO, 0); + break; +#endif } } @@ -24628,6 +24636,265 @@ void clif_broadcast_refine_result(map_session_data& sd, t_itemid itemId, int8 le #endif } +void clif_parse_captcha_register(int fd, map_session_data *sd) { +#if PACKETVER >= 20160316 + nullpo_retv(sd); + + if (!pc_has_permission(sd, PC_PERM_MACRO_REGISTER)) { + clif_displaymessage(sd->fd, msg_txt(sd, 246)); // Your GM level doesn't authorize you to perform this action. + return; + } + + PACKET_CZ_REQ_UPLOAD_MACRO_DETECTOR *p = (PACKET_CZ_REQ_UPLOAD_MACRO_DETECTOR *)RFIFOP(fd, 0); + + pc_macro_captcha_register(*sd, p->imageSize, p->answer); +#endif +} + +void clif_captcha_upload_request(map_session_data &sd) { +#if PACKETVER >= 20160330 + PACKET_ZC_ACK_UPLOAD_MACRO_DETECTOR p = {}; + + p.PacketType = HEADER_ZC_ACK_UPLOAD_MACRO_DETECTOR; + safestrncpy(p.captchaKey, "", sizeof(p.captchaKey)); + if (sd.captcha_upload.cd != nullptr) { + p.captchaFlag = 0; + } else { + p.captchaFlag = 1; + } + + clif_send(&p, sizeof(p), &sd.bl, SELF); +#endif +} + +void clif_parse_captcha_upload(int fd, map_session_data *sd) { +#if PACKETVER >= 20160316 + nullpo_retv(sd); + + if (!pc_has_permission(sd, PC_PERM_MACRO_REGISTER)) { + clif_displaymessage(sd->fd, msg_txt(sd, 246)); // Your GM level doesn't authorize you to perform this action. + return; + } + + PACKET_CZ_UPLOAD_MACRO_DETECTOR_CAPTCHA *p = (PACKET_CZ_UPLOAD_MACRO_DETECTOR_CAPTCHA *)RFIFOP(fd, 0); + int16 upload_size = p->PacketLength - sizeof(PACKET_CZ_UPLOAD_MACRO_DETECTOR_CAPTCHA); + + if (upload_size < 1 || upload_size > MAX_CAPTCHA_CHUNK_SIZE) + return; + + if (sd->captcha_upload.upload_size + upload_size > sd->captcha_upload.cd->image_size) + return; + + pc_macro_captcha_register_upload(*sd, upload_size, p->imageData); +#endif +} + +void clif_captcha_upload_end(map_session_data &sd) { +#if PACKETVER >= 20160330 + PACKET_ZC_COMPLETE_UPLOAD_MACRO_DETECTOR_CAPTCHA p = {}; + + p.PacketType = HEADER_ZC_COMPLETE_UPLOAD_MACRO_DETECTOR_CAPTCHA; + + clif_send(&p, sizeof(p), &sd.bl, SELF); +#endif +} + +void clif_parse_captcha_preview_request(int fd, map_session_data *sd) { +#if PACKETVER >= 20160323 + nullpo_retv(sd); + + if (!(pc_has_permission(sd, PC_PERM_MACRO_REGISTER) && pc_has_permission(sd, PC_PERM_MACRO_DETECT))) { + clif_displaymessage(sd->fd, msg_txt(sd, 246)); // Your GM level doesn't authorize you to perform this action. + return; + } + + PACKET_CZ_REQ_PREVIEW_MACRO_DETECTOR *p = (PACKET_CZ_REQ_PREVIEW_MACRO_DETECTOR *)RFIFOP(fd, 0); + + clif_captcha_preview_response(*sd, captcha_db.find(p->captchaID)); +#endif +} + +void clif_captcha_preview_response(map_session_data &sd, std::shared_ptr cd) { +#if PACKETVER >= 20160330 + PACKET_ZC_ACK_PREVIEW_MACRO_DETECTOR p = {}; + + p.PacketType = HEADER_ZC_ACK_PREVIEW_MACRO_DETECTOR; + safestrncpy(p.captchaKey, "", sizeof(p.captchaKey)); + if (cd == nullptr) { + p.captchaFlag = 1; + p.imageSize = 0; + } else { + p.captchaFlag = 0; + p.imageSize = cd->image_size; + } + + clif_send(&p, sizeof(p), &sd.bl, SELF); + + if (cd != nullptr) { + for (uint16 offset = 0; offset < cd->image_size;) { + uint16 chunk_size = min(cd->image_size - offset, MAX_CAPTCHA_CHUNK_SIZE); + PACKET_ZC_PREVIEW_MACRO_DETECTOR_CAPTCHA *p2 = (PACKET_ZC_PREVIEW_MACRO_DETECTOR_CAPTCHA *)packet_buffer; + + p2->PacketType = HEADER_ZC_PREVIEW_MACRO_DETECTOR_CAPTCHA; + p2->PacketLength = (int16)(sizeof(PACKET_ZC_PREVIEW_MACRO_DETECTOR_CAPTCHA) + chunk_size); + safestrncpy(p2->captchaKey, p.captchaKey, sizeof(p2->captchaKey)); + memcpy(p2->imageData, &cd->image_data[offset], chunk_size); + + clif_send(p2, p2->PacketLength, &sd.bl, SELF); + + offset += chunk_size; + } + } +#endif +} + +void clif_macro_detector_request(map_session_data &sd) { +#if PACKETVER >= 20160330 + std::shared_ptr cd = sd.macro_detect.cd; + + if (cd == nullptr) { + return; + } + + // Send preview initialization request to the client. + PACKET_ZC_APPLY_MACRO_DETECTOR p = {}; + + p.PacketType = HEADER_ZC_APPLY_MACRO_DETECTOR; + p.imageSize = cd->image_size; + safestrncpy(p.captchaKey, "", sizeof(p.captchaKey)); + + clif_send(&p, sizeof(p), &sd.bl, SELF); + + for (uint16 offset = 0; offset < cd->image_size;) { + uint16 chunk_size = min(cd->image_size - offset, MAX_CAPTCHA_CHUNK_SIZE); + PACKET_ZC_APPLY_MACRO_DETECTOR_CAPTCHA *p2 = (PACKET_ZC_APPLY_MACRO_DETECTOR_CAPTCHA *)packet_buffer; + + p2->PacketType = HEADER_ZC_APPLY_MACRO_DETECTOR_CAPTCHA; + p2->PacketLength = (int16)(sizeof(PACKET_ZC_APPLY_MACRO_DETECTOR_CAPTCHA) + chunk_size); + safestrncpy(p2->captchaKey, p.captchaKey, sizeof(p2->captchaKey)); + memcpy(p2->imageData, &cd->image_data[offset], chunk_size); + + clif_send(p2, p2->PacketLength, &sd.bl, SELF); + + offset += chunk_size; + } +#endif +} + +void clif_macro_detector_request_show(map_session_data &sd) { +#if PACKETVER >= 20160330 + PACKET_ZC_REQ_ANSWER_MACRO_DETECTOR p = {}; + + p.PacketType = HEADER_ZC_REQ_ANSWER_MACRO_DETECTOR; + p.retryCount = sd.macro_detect.retry; + p.timeout = battle_config.macro_detection_timeout; + + clif_send(&p, sizeof(p), &sd.bl, SELF); +#endif +} + +void clif_parse_macro_detector_download_ack(int fd, map_session_data *sd) { +#if PACKETVER >= 20160316 + nullpo_retv(sd); + + if (sd->macro_detect.retry != 0) { + //PACKET_CZ_COMPLETE_APPLY_MACRO_DETECTOR_CAPTCHA *p = (PACKET_CZ_COMPLETE_APPLY_MACRO_DETECTOR_CAPTCHA *)RFIFOP(fd, 0); + + clif_macro_detector_request_show(*sd); + } +#endif +} + +void clif_parse_macro_detector_answer(int fd, map_session_data *sd) { +#if PACKETVER >= 20160316 + nullpo_retv(sd); + + PACKET_CZ_ACK_ANSWER_MACRO_DETECTOR *p = (PACKET_CZ_ACK_ANSWER_MACRO_DETECTOR *)RFIFOP(fd, 0); + + pc_macro_detector_process_answer(*sd, p->answer); +#endif +} + +void clif_macro_detector_status(map_session_data &sd, e_macro_detect_status stype) { +#if PACKETVER >= 20160330 + PACKET_ZC_CLOSE_MACRO_DETECTOR p = {}; + + p.PacketType = HEADER_ZC_CLOSE_MACRO_DETECTOR; + p.status = stype; + + clif_send(&p, sizeof(p), &sd.bl, SELF); +#endif +} + +void clif_parse_macro_reporter_select(int fd, map_session_data *sd) { +#if PACKETVER >= 20160330 + nullpo_retv(sd); + + if (!pc_has_permission(sd, PC_PERM_MACRO_DETECT)) { + clif_displaymessage(sd->fd, msg_txt(sd, 246)); // Your GM level doesn't authorize you to perform this action. + return; + } + + PACKET_CZ_REQ_PLAYER_AID_IN_RANGE *p = (PACKET_CZ_REQ_PLAYER_AID_IN_RANGE *)RFIFOP(fd, 0); + + pc_macro_reporter_area_select(*sd, p->xPos, p->yPos, p->RadiusRange); +#endif +} + +void clif_macro_reporter_select(map_session_data &sd, const std::vector &aid_list) { +#if PACKETVER >= 20160330 + PACKET_ZC_ACK_PLAYER_AID_IN_RANGE *p = (PACKET_ZC_ACK_PLAYER_AID_IN_RANGE *)packet_buffer; + + p->PacketType = HEADER_ZC_ACK_PLAYER_AID_IN_RANGE; + p->PacketLength = static_cast(sizeof(PACKET_ZC_ACK_PLAYER_AID_IN_RANGE) + sizeof(uint32) * aid_list.size()); + for (size_t i = 0; i < aid_list.size(); i++) + p->AID[i] = aid_list[static_cast(i)]; + + clif_send(p, p->PacketLength, &sd.bl, SELF); +#endif +} + +void clif_parse_macro_reporter_ack(int fd, map_session_data *sd) { +#if PACKETVER >= 20160316 + nullpo_retv(sd); + + if (!pc_has_permission(sd, PC_PERM_MACRO_DETECT)) { + clif_displaymessage(sd->fd, msg_txt(sd, 246)); // Your GM level doesn't authorize you to perform this action. + return; + } + + PACKET_CZ_REQ_APPLY_MACRO_DETECTOR *p = (PACKET_CZ_REQ_APPLY_MACRO_DETECTOR *)RFIFOP(fd, 0); + map_session_data *tsd = map_id2sd(p->AID); + + if (tsd == nullptr) { + clif_displaymessage(fd, msg_txt(sd, 3)); // Character not found. + return; + } + if (tsd->macro_detect.retry != 0) { + clif_macro_reporter_status(*sd, MCR_INPROGRESS); + return; + } + if (captcha_db.empty()) { + clif_macro_reporter_status(*sd, MCR_NO_DATA); + return; + } + + pc_macro_reporter_process(*sd, *tsd); + clif_macro_reporter_status(*sd, MCR_MONITORING); +#endif +} + +void clif_macro_reporter_status(map_session_data &sd, e_macro_report_status stype) { +#if PACKETVER >= 20160330 + PACKET_ZC_ACK_APPLY_MACRO_DETECTOR p = {}; + + p.PacketType = HEADER_ZC_ACK_APPLY_MACRO_DETECTOR; + p.status = stype; + + clif_send(&p, sizeof(p), &sd.bl, SELF); +#endif +} + /*========================================== * Main client packet processing function *------------------------------------------*/ diff --git a/src/map/clif.hpp b/src/map/clif.hpp index 113963a87a..878a75cb78 100644 --- a/src/map/clif.hpp +++ b/src/map/clif.hpp @@ -47,6 +47,9 @@ enum e_bg_queue_apply_ack : uint16; enum e_instance_notify : uint8; struct s_laphine_synthesis; struct s_laphine_upgrade; +struct s_captcha_data; +enum e_macro_detect_status : uint8; +enum e_macro_report_status : uint8; enum e_PacketDBVersion { // packet DB MIN_PACKET_DB = 0x064, @@ -1157,12 +1160,16 @@ void clif_achievement_reward_ack(int fd, unsigned char result, int ach_id); /// Attendance System enum in_ui_type : int8 { + IN_UI_MACRO_REGISTER = 2, + IN_UI_MACRO_DETECTOR, IN_UI_ATTENDANCE = 5 }; enum out_ui_type : int8 { OUT_UI_BANK = 0, OUT_UI_STYLIST, + OUT_UI_CAPTCHA, + OUT_UI_MACRO, OUT_UI_QUEST = 6, OUT_UI_ATTENDANCE, OUT_UI_ENCHANTGRADE, @@ -1217,4 +1224,20 @@ void clif_enchantingshadow_spirit(unit_data &ud); void clif_broadcast_refine_result(struct map_session_data& sd, t_itemid itemId, int8 level, bool success); +// Captcha Register +void clif_captcha_upload_request(map_session_data &sd); +void clif_captcha_upload_end(map_session_data &sd); + +// Captcha Preview +void clif_captcha_preview_response(map_session_data &sd, std::shared_ptr cd); + +// Macro Detector +void clif_macro_detector_request(map_session_data &sd); +void clif_macro_detector_request_show(map_session_data &sd); +void clif_macro_detector_status(map_session_data &sd, e_macro_detect_status stype); + +// Macro Reporter +void clif_macro_reporter_select(map_session_data &sd, const std::vector &aid_list); +void clif_macro_reporter_status(map_session_data &sd, e_macro_report_status stype); + #endif /* CLIF_HPP */ diff --git a/src/map/clif_packetdb.hpp b/src/map/clif_packetdb.hpp index 3f9081a1ba..ce355db155 100644 --- a/src/map/clif_packetdb.hpp +++ b/src/map/clif_packetdb.hpp @@ -2246,6 +2246,20 @@ packet(0x0A51,34); #endif +#if PACKETVER >= 20160316 + parseable_packet(HEADER_CZ_REQ_UPLOAD_MACRO_DETECTOR, sizeof(PACKET_CZ_REQ_UPLOAD_MACRO_DETECTOR), clif_parse_captcha_register, 0); + parseable_packet(HEADER_CZ_UPLOAD_MACRO_DETECTOR_CAPTCHA, -1, clif_parse_captcha_upload, 0); + parseable_packet(HEADER_CZ_COMPLETE_APPLY_MACRO_DETECTOR_CAPTCHA, sizeof(PACKET_CZ_COMPLETE_APPLY_MACRO_DETECTOR_CAPTCHA), clif_parse_macro_detector_download_ack, 0); + parseable_packet(HEADER_CZ_ACK_ANSWER_MACRO_DETECTOR, sizeof(PACKET_CZ_ACK_ANSWER_MACRO_DETECTOR), clif_parse_macro_detector_answer, 0); + parseable_packet(HEADER_CZ_REQ_APPLY_MACRO_DETECTOR, sizeof(PACKET_CZ_REQ_APPLY_MACRO_DETECTOR), clif_parse_macro_reporter_ack, 0); +#endif +#if PACKETVER >= 20160323 + parseable_packet(HEADER_CZ_REQ_PREVIEW_MACRO_DETECTOR, sizeof(PACKET_CZ_REQ_PREVIEW_MACRO_DETECTOR), clif_parse_captcha_preview_request, 0); +#endif +#if PACKETVER >= 20160330 + parseable_packet(HEADER_CZ_REQ_PLAYER_AID_IN_RANGE, sizeof(PACKET_CZ_REQ_PLAYER_AID_IN_RANGE), clif_parse_macro_reporter_select, 0); +#endif + // 2016-03-30aRagexe #if PACKETVER >= 20160330 parseable_packet(0x0A6E,-1,clif_parse_Mail_send,2,4,28,52,60,62,64,68); // CZ_REQ_WRITE_MAIL2 diff --git a/src/map/map-server.vcxproj b/src/map/map-server.vcxproj index 3fbfc1d389..943bc5d2f2 100644 --- a/src/map/map-server.vcxproj +++ b/src/map/map-server.vcxproj @@ -320,6 +320,7 @@ + diff --git a/src/map/map.cpp b/src/map/map.cpp index 4ae9436af3..8532887b42 100644 --- a/src/map/map.cpp +++ b/src/map/map.cpp @@ -2172,6 +2172,7 @@ int map_quit(struct map_session_data *sd) { pc_makesavestatus(sd); pc_clean_skilltree(sd); pc_crimson_marker_clear(sd); + pc_macro_detector_disconnect(*sd); chrif_save(sd, CSAVE_QUIT|CSAVE_INVENTORY|CSAVE_CART); unit_free_pc(sd); return 0; diff --git a/src/map/packets_struct.hpp b/src/map/packets_struct.hpp index bf32c2e7c0..3a7d5767cf 100644 --- a/src/map/packets_struct.hpp +++ b/src/map/packets_struct.hpp @@ -5000,7 +5000,7 @@ DEFINE_PACKET_HEADER(ZC_MYGUILD_BASIC_INFO, 0x014c); struct PACKET_CZ_REQ_UPLOAD_MACRO_DETECTOR { int16 PacketType; char answer[16]; - int16 imageSize; + uint16 imageSize; } __attribute__((packed)); DEFINE_PACKET_HEADER(CZ_REQ_UPLOAD_MACRO_DETECTOR, 0x0a52); #endif @@ -5050,7 +5050,7 @@ DEFINE_PACKET_HEADER(ZC_ACK_APPLY_MACRO_DETECTOR, 0x0a57); #if PACKETVER >= 20160330 struct PACKET_ZC_APPLY_MACRO_DETECTOR { int16 PacketType; - int16 imageSize; + uint16 imageSize; char captchaKey[4]; } __attribute__((packed)); DEFINE_PACKET_HEADER(ZC_APPLY_MACRO_DETECTOR, 0x0a58); @@ -5110,7 +5110,7 @@ DEFINE_PACKET_HEADER(CZ_REQ_PREVIEW_MACRO_DETECTOR, 0x0a69); struct PACKET_ZC_ACK_PREVIEW_MACRO_DETECTOR { int16 PacketType; int captchaFlag; - int16 imageSize; + uint16 imageSize; char captchaKey[4]; } __attribute__((packed)); DEFINE_PACKET_HEADER(ZC_ACK_PREVIEW_MACRO_DETECTOR, 0x0a6a); diff --git a/src/map/party.cpp b/src/map/party.cpp index bd5e2fac4d..42b7e5a349 100644 --- a/src/map/party.cpp +++ b/src/map/party.cpp @@ -418,7 +418,7 @@ int party_invite(struct map_session_data *sd,struct map_session_data *tsd) // confirm whether the account has the ability to invite before checking the player if( !pc_has_permission(sd, PC_PERM_PARTY) || (tsd && !pc_has_permission(tsd, PC_PERM_PARTY)) ) { - clif_displaymessage(sd->fd, msg_txt(sd,81)); // "Your GM level doesn't authorize you to preform this action on the specified player." + clif_displaymessage(sd->fd, msg_txt(sd,81)); // "Your GM level doesn't authorize you to perform this action on the specified player." return 0; } diff --git a/src/map/pc.cpp b/src/map/pc.cpp index 67fb7346a0..d5cdc281ef 100755 --- a/src/map/pc.cpp +++ b/src/map/pc.cpp @@ -12,6 +12,7 @@ #include "../common/core.hpp" // get_svn_revision() #include "../common/database.hpp" #include "../common/ers.hpp" // ers_destroy +#include "../common/grfio.hpp" #include "../common/malloc.hpp" #include "../common/mmo.hpp" //NAME_LENGTH #include "../common/nullpo.hpp" @@ -61,6 +62,9 @@ using namespace rathena; JobDatabase job_db; +CaptchaDatabase captcha_db; +const char *macro_allowed_answer_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + int pc_split_atoui(char* str, unsigned int* val, char sep, int max); static inline bool pc_attendance_rewarded_today( struct map_session_data* sd ); @@ -1747,6 +1751,7 @@ bool pc_authok(struct map_session_data *sd, uint32 login_id2, time_t expiration_ sd->autotrade_tid = INVALID_TIMER; sd->respawn_tid = INVALID_TIMER; sd->tid_queue_active = INVALID_TIMER; + sd->macro_detect.timer = INVALID_TIMER; sd->skill_keep_using.tid = INVALID_TIMER; sd->skill_keep_using.skill_id = 0; @@ -14989,6 +14994,351 @@ void pc_attendance_claim_reward( struct map_session_data* sd ){ clif_attendence_response( sd, attendance_counter ); } +/** + * Save a captcha image to memory via /macro_register. + * @param sd: Player data + * @param image_size: Captcha image size + * @param captcha_answer: Answer to captcha + */ +void pc_macro_captcha_register(map_session_data &sd, uint16 image_size, char captcha_answer[CAPTCHA_ANSWER_SIZE]) { + nullpo_retv(captcha_answer); + + sd.captcha_upload.cd = nullptr; + sd.captcha_upload.upload_size = 0; + + if (strlen(captcha_answer) < 4 || image_size == 0 || image_size > CAPTCHA_BMP_SIZE) { + clif_captcha_upload_request(sd); // Notify client of failure. + return; + } + + std::shared_ptr cd = std::make_shared(); + sd.captcha_upload.cd = cd; + + cd->image_size = image_size; + safestrncpy(cd->captcha_answer, captcha_answer, sizeof(cd->captcha_answer)); + memset(cd->image_data, 0, sizeof(cd->image_data)); + + // Request the image data from the client. + clif_captcha_upload_request(sd); +} + +/** + * Save captcha image to server. + * @param sd: Player data + * @param captcha_key: Captcha ID + * @param upload_size: Captcha size + * @param upload_data: Image data + */ +void pc_macro_captcha_register_upload(map_session_data &sd, uint16 upload_size, char *upload_data) { + nullpo_retv(upload_data); + + memcpy(&sd.captcha_upload.cd->image_data[sd.captcha_upload.upload_size], upload_data, upload_size); + sd.captcha_upload.upload_size += upload_size; + + // Notify that the image finished uploading. + if (sd.captcha_upload.upload_size == sd.captcha_upload.cd->image_size) { + // Tell the client that the upload was finished + clif_captcha_upload_end(sd); + + // Look for a free key + uint16 index; + + for (index = 0; index < UINT16_MAX; index++) { + if (!captcha_db.exists(index)) { + break; + } + } + + if (index == UINT16_MAX) { + // no free key found... + sd.captcha_upload.cd = nullptr; + sd.captcha_upload.upload_size = 0; + return; + } + + captcha_db.put(index, sd.captcha_upload.cd); + sd.captcha_upload.cd = nullptr; + sd.captcha_upload.upload_size = 0; + + // TODO: write YAML and BMP file? + } +} + +/** + * Timer attached to target player with attempts to confirm captcha. + */ +TIMER_FUNC(pc_macro_detector_timeout) { + map_session_data *sd = map_id2sd(id); + + nullpo_ret(sd); + + // Remove the current timer + sd->macro_detect.timer = INVALID_TIMER; + + // Deduct an answering attempt + sd->macro_detect.retry -= 1; + + if (sd->macro_detect.retry == 0) { + // All attempts have been exhausted block the user + clif_macro_detector_status(*sd, MCD_TIMEOUT); + chrif_req_login_operation(sd->macro_detect.reporter_aid, sd->status.name, CHRIF_OP_LOGIN_BLOCK, 0, 0, 0); + } else { + // Update the client + clif_macro_detector_request_show(*sd); + + // Start a new timer + sd->macro_detect.timer = add_timer(gettick() + battle_config.macro_detection_timeout, pc_macro_detector_timeout, sd->bl.id, 0); + } + return 0; +} + +/** + * Check player's captcha answer. + * @param sd: Player data + * @param captcha_answer: Captcha answer entered by player + */ +void pc_macro_detector_process_answer(map_session_data &sd, char captcha_answer[CAPTCHA_ANSWER_SIZE]) { + nullpo_retv(captcha_answer); + + const std::shared_ptr cd = sd.macro_detect.cd; + + // Has no captcha request + if (cd == nullptr) { + return; + } + + // Correct answer + if (strcmp(captcha_answer, cd->captcha_answer) == 0) { + // Delete the timer + delete_timer(sd.macro_detect.timer, pc_macro_detector_timeout); + + // Clear the macro detect data + sd.macro_detect = {}; + sd.macro_detect.timer = INVALID_TIMER; + + // Unblock all actions for the player + sd.state.block_action &= ~PCBLOCK_ALL; + sd.state.block_action &= ~PCBLOCK_IMMUNE; + + // Assign temporary macro variable to check failures + pc_setreg(&sd, add_str("@captcha_retries"), battle_config.macro_detection_retry - sd.macro_detect.retry); + + // Grant bonuses via script + run_script(cd->bonus_script, 0, sd.bl.id, fake_nd->bl.id); + + // Notify the client + clif_macro_detector_status(sd, MCD_GOOD); + } else { + // Deduct an answering attempt + sd.macro_detect.retry -= 1; + + // All attempts have been exhausted block the user + if (sd.macro_detect.retry <= 0) { + clif_macro_detector_status(sd, MCD_INCORRECT); + chrif_req_login_operation(sd.macro_detect.reporter_aid, sd.status.name, CHRIF_OP_LOGIN_BLOCK, 0, 0, 0); + return; + } + + // Incorrect response, update the client + clif_macro_detector_request_show(sd); + + // Reset the timer + addtick_timer(sd.macro_detect.timer, gettick() + battle_config.macro_detection_timeout); + } +} + +/** + * Determine if a player tries to log out during a captcha check. + * @param sd: Player data + */ +void pc_macro_detector_disconnect(map_session_data &sd) { + // Delete the timeout timer + if (sd.macro_detect.timer != INVALID_TIMER) { + delete_timer(sd.macro_detect.timer, pc_macro_detector_timeout); + sd.macro_detect.timer = INVALID_TIMER; + } + + // If the player disconnects before clearing the challenge the account is banned. + if (sd.macro_detect.retry != 0) + chrif_req_login_operation(sd.macro_detect.reporter_aid, sd.status.name, CHRIF_OP_LOGIN_BLOCK, 0, 0, 0); +} + +/** + * Save a list of players from an area select via /macro_detector. + */ +int pc_macro_reporter_area_select_sub(block_list *bl, va_list ap) { + nullpo_retr(0, bl); + + if (bl->type != BL_PC) + return 0; + + std::vector *aid_list = va_arg(ap, std::vector *); + + nullpo_ret(aid_list); + + aid_list->push_back(bl->id); + return 0; +} + +/** + * Area select via /macro_detector. + * @param sd: Player data + * @param x: X location + * @param y: Y location + * @param radius: Area + */ +void pc_macro_reporter_area_select(map_session_data &sd, const int16 x, const int16 y, const int8 radius) { + std::vector aid_list; + + map_foreachinarea(pc_macro_reporter_area_select_sub, sd.bl.m, x - radius, y - radius, x + radius, y + radius, BL_PC, &aid_list); + + clif_macro_reporter_select(sd, aid_list); +} + +/** + * Send out captcha check to player. + * @param ssd: Source player data + * @param tsd: Target player data + */ +void pc_macro_reporter_process(map_session_data &ssd, map_session_data &tsd) { + if (captcha_db.empty()) + return; + + // Pick a random image from the database. + const std::shared_ptr cd = captcha_db.random(); + + // Set macro detection data. + tsd.macro_detect.cd = cd; + tsd.macro_detect.reporter_aid = ssd.status.account_id; + tsd.macro_detect.retry = battle_config.macro_detection_retry; + + // Block all actions for the target player. + tsd.state.block_action |= (PCBLOCK_ALL | PCBLOCK_IMMUNE); + + // Open macro detect client side. + clif_macro_detector_request(tsd); + + // Start the timeout timer. + tsd.macro_detect.timer = add_timer(gettick() + battle_config.macro_detection_timeout, pc_macro_detector_timeout, tsd.bl.id, 0); +} + +/** + * Parse a BMP image to memory. + * @param filepath: Image file location + * @param cd: Captcha data + */ +bool pc_macro_read_captcha_db_loadbmp(const std::string &filepath, std::shared_ptr cd) { + if (cd == nullptr) + return false; + + FILE *fp = fopen(filepath.c_str(), "rb"); + + if (fp == nullptr) { + ShowError("%s: Failed to open file \"%s\"\n", __func__, filepath.c_str()); + return false; + } + + // Load the file data and verify magic + char bmp_data[CAPTCHA_BMP_SIZE]; + + if (fread(bmp_data, CAPTCHA_BMP_SIZE, 1, fp) != 1) { + ShowError("%s: Failed to read data from \"%s\"\n", __func__, filepath.c_str()); + fclose(fp); + return false; + } + + fclose(fp); + + if (bmp_data[0] != 'B' || bmp_data[1] != 'M') { + ShowError("%s: Invalid BMP file header given at \"%s\"\n", __func__, filepath.c_str()); + return false; + } + + // Compress the data into the destination + unsigned long com_size = sizeof(cd->image_data); + + encode_zip(cd->image_data, &com_size, bmp_data, CAPTCHA_BMP_SIZE); + cd->image_size = static_cast(com_size); + + return true; +} + +const std::string CaptchaDatabase::getDefaultLocation() { + return std::string(db_path) + "/captcha_db.yml"; +} + +/** + * Reads and parses an entry from the captcha_db. + * @param node: YAML node containing the entry. + * @return count of successfully parsed rows + */ +uint64 CaptchaDatabase::parseBodyNode(const ryml::NodeRef &node) { + uint16 index; + + if (!this->asUInt16(node, "Id", index)) + return 0; + + std::shared_ptr cd = captcha_db.find(index); + bool exists = cd != nullptr; + + if (!exists) { + if (!this->nodesExist(node, { "Filename", "Answer" })) + return 0; + + cd = std::make_shared(); + cd->index = index; + } + + if (this->nodeExists(node, "Filename")) { + std::string filename; + + if (!this->asString(node, "Filename", filename)) + return 0; + + if (!pc_macro_read_captcha_db_loadbmp(filename, cd)) { + this->invalidWarning(node["Filename"], "Failed to parse BMP image, skipping...\n"); + return 0; + } + } + + if (this->nodeExists(node, "Answer")) { + std::string answer; + + if (!this->asString(node, "Answer", answer)) + return 0; + + if (answer.length() < 4 || answer.length() > CAPTCHA_ANSWER_SIZE) { + this->invalidWarning(node["Answer"], "The captcha answer must be between 4~%d characters, skipping...", CAPTCHA_ANSWER_SIZE); + return 0; + } + + safestrncpy(cd->captcha_answer, answer.c_str(), sizeof(cd->captcha_answer)); + } + + if (this->nodeExists(node, "Bonus")) { + std::string script; + + if (!this->asString(node, "Bonus", script)) { + return 0; + } + + if (cd->bonus_script) { + script_free_code(cd->bonus_script); + cd->bonus_script = nullptr; + } + + cd->bonus_script = parse_script(script.c_str(), this->getCurrentFile().c_str(), this->getLineNumber(node["Bonus"]), SCRIPT_IGNORE_EXTERNAL_BRACKETS); + } else { + if (!exists) + cd->bonus_script = parse_script("specialeffect2 EF_BLESSING; sc_start SC_BLESSING,600000,10; specialeffect2 EF_INCAGILITY; sc_start SC_INCREASEAGI,600000,10;", "macro_script", 0, SCRIPT_IGNORE_EXTERNAL_BRACKETS); + } + + if (!exists) + captcha_db.put(index, cd); + + return 1; +} + /*========================================== * pc Init/Terminate *------------------------------------------*/ @@ -15003,6 +15353,7 @@ void do_final_pc(void) { attendance_db.clear(); reputation_db.clear(); penalty_db.clear(); + captcha_db.clear(); } void do_init_pc(void) { @@ -15013,6 +15364,7 @@ void do_init_pc(void) { pc_read_motd(); // Read MOTD [Valaris] attendance_db.load(); reputation_db.load(); + captcha_db.load(); add_timer_func_list(pc_invincible_timer, "pc_invincible_timer"); add_timer_func_list(pc_eventtimer, "pc_eventtimer"); @@ -15027,6 +15379,7 @@ void do_init_pc(void) { add_timer_func_list(pc_expiration_timer, "pc_expiration_timer"); add_timer_func_list(pc_autotrade_timer, "pc_autotrade_timer"); add_timer_func_list(pc_on_expire_active, "pc_on_expire_active"); + add_timer_func_list(pc_macro_detector_timeout, "pc_macro_detector_timeout"); add_timer(gettick() + autosave_interval, pc_autosave, 0, 0); diff --git a/src/map/pc.hpp b/src/map/pc.hpp index 655592a69c..d7f647bc2f 100644 --- a/src/map/pc.hpp +++ b/src/map/pc.hpp @@ -120,6 +120,60 @@ enum e_additem_result : uint8 { ADDITEM_STACKLIMIT }; +#ifndef CAPTCHA_ANSWER_SIZE + #define CAPTCHA_ANSWER_SIZE 16 +#endif +#ifndef CAPTCHA_BMP_SIZE + #define CAPTCHA_BMP_SIZE (2 + 52 + (3 * 220 * 90)) // sizeof("BM") + sizeof(BITMAPV2INFOHEADER) + 24bits 220x90 BMP +#endif +#ifndef MAX_CAPTCHA_CHUNK_SIZE + #define MAX_CAPTCHA_CHUNK_SIZE 1024 +#endif + +struct s_captcha_data { + uint16 index; + uint16 image_size; + char image_data[CAPTCHA_BMP_SIZE]; + char captcha_answer[CAPTCHA_ANSWER_SIZE]; + script_code *bonus_script; + + ~s_captcha_data() { + if (this->bonus_script) + script_free_code(this->bonus_script); + } +}; + +struct s_macro_detect { + std::shared_ptr cd; + int32 reporter_aid; + int32 retry; + int32 timer; +}; + +enum e_macro_detect_status : uint8 { + MCD_TIMEOUT = 0, + MCD_INCORRECT = 1, + MCD_GOOD = 2, +}; + +enum e_macro_report_status : uint8 { + MCR_MONITORING = 0, + MCR_NO_DATA = 1, + MCR_INPROGRESS = 2, +}; + +class CaptchaDatabase : public TypesafeYamlDatabase { +public: + CaptchaDatabase() : TypesafeYamlDatabase("CAPTCHA_DB", 1) { + + } + + const std::string getDefaultLocation() override; + uint64 parseBodyNode(const ryml::NodeRef &node) override; +}; + +extern CaptchaDatabase captcha_db; + struct skill_cooldown_entry { unsigned short skill_id; int timer; @@ -869,6 +923,13 @@ struct map_session_data { uint16 level; int target; } skill_keep_using; + + struct { + std::shared_ptr cd; + uint16 upload_size; + } captcha_upload; + + s_macro_detect macro_detect; }; extern struct eri *pc_sc_display_ers; /// Player's SC display table @@ -1632,4 +1693,16 @@ bool pc_attendance_enabled(); int32 pc_attendance_counter( struct map_session_data* sd ); void pc_attendance_claim_reward( struct map_session_data* sd ); +// Captcha Register +void pc_macro_captcha_register(map_session_data &sd, uint16 image_size, char captcha_answer[CAPTCHA_ANSWER_SIZE]); +void pc_macro_captcha_register_upload(map_session_data & sd, uint16 upload_size, char *upload_data); + +// Macro Detector +void pc_macro_detector_process_answer(map_session_data &sd, char captcha_answer[CAPTCHA_ANSWER_SIZE]); +void pc_macro_detector_disconnect(map_session_data &sd); + +// Macro Reporter +void pc_macro_reporter_area_select(map_session_data &sd, const int16 x, const int16 y, const int8 radius); +void pc_macro_reporter_process(map_session_data &ssd, map_session_data &tsd); + #endif /* PC_HPP */ diff --git a/src/map/pc_groups.hpp b/src/map/pc_groups.hpp index de621ee2a6..e299144e7a 100644 --- a/src/map/pc_groups.hpp +++ b/src/map/pc_groups.hpp @@ -48,6 +48,8 @@ enum e_pc_permission : uint32 { PC_PERM_BYPASS_STAT_ONCLONE, PC_PERM_BYPASS_MAX_STAT, PC_PERM_ATTENDANCE, + PC_PERM_MACRO_DETECT, + PC_PERM_MACRO_REGISTER, //.. add other here PC_PERM_MAX, }; @@ -84,6 +86,8 @@ static const struct s_pcg_permission_name { { "bypass_stat_onclone",PC_PERM_BYPASS_STAT_ONCLONE }, { "bypass_max_stat",PC_PERM_BYPASS_MAX_STAT }, { "attendance",PC_PERM_ATTENDANCE }, + { "macro_detect",PC_PERM_MACRO_DETECT }, + { "macro_register",PC_PERM_MACRO_REGISTER }, }; struct s_player_group{