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 <lemongrass@kstp.at>
This commit is contained in:
Aleos 2022-10-14 10:54:19 -04:00 committed by GitHub
parent 750c7d72de
commit d7bf5ebb58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 883 additions and 5 deletions

View File

@ -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

View File

@ -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:

37
db/captcha_db.yml Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#
###########################################################################
# 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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
###########################################################################
# 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

44
doc/captcha_db.txt Normal file
View File

@ -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;
}

View File

@ -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).
---------------------------------------

View File

@ -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;
}

View File

@ -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"
};

View File

@ -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"
};

View File

@ -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<s_captcha_data> 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<s_captcha_data> 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<uint32> &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<int16>(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<int32>(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
*------------------------------------------*/

View File

@ -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<s_captcha_data> 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<uint32> &aid_list);
void clif_macro_reporter_status(map_session_data &sd, e_macro_report_status stype);
#endif /* CLIF_HPP */

View File

@ -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

View File

@ -320,6 +320,7 @@
<Copy SourceFiles="$(SolutionDir)db\import-tmpl\attendance.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\attendance.yml')" />
<Copy SourceFiles="$(SolutionDir)db\import-tmpl\attr_fix.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\attr_fix.yml')" />
<Copy SourceFiles="$(SolutionDir)db\import-tmpl\battleground_db.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\battleground_db.yml')" />
<Copy SourceFiles="$(SolutionDir)db\import-tmpl\captcha_db.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\captcha_db.yml')" />
<Copy SourceFiles="$(SolutionDir)db\import-tmpl\castle_db.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\castle_db.yml')" />
<Copy SourceFiles="$(SolutionDir)db\import-tmpl\const.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\const.yml')" />
<Copy SourceFiles="$(SolutionDir)db\import-tmpl\create_arrow_db.yml" DestinationFolder="$(SolutionDir)db\import\" ContinueOnError="true" Condition="!Exists('$(SolutionDir)db\import\create_arrow_db.yml')" />

View File

@ -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;

View File

@ -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);

View File

@ -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;
}

View File

@ -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<s_captcha_data> cd = std::make_shared<s_captcha_data>();
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<s_captcha_data> 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<uint32> *aid_list = va_arg(ap, std::vector<uint32> *);
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<uint32> 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<s_captcha_data> 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<s_captcha_data> 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<int16>(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<s_captcha_data> cd = captcha_db.find(index);
bool exists = cd != nullptr;
if (!exists) {
if (!this->nodesExist(node, { "Filename", "Answer" }))
return 0;
cd = std::make_shared<s_captcha_data>();
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);

View File

@ -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<s_captcha_data> 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<int16, s_captcha_data> {
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<s_captcha_data> 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 */

View File

@ -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{