Ochrona przed botami dla gier

Gracze klikają kilka obrazków. Boty się zacinają. Twój serwer gry sam podpisuje i sprawdza każdy token, więc zmiana zasad nie wymaga aktualizacji serwera.

Podgląd i instalacja

Wypróbuj

Przykładowe pytanie. Zaznacz wszystkie pasujące zwierzęta i naciśnij Dalej.

Jak to działa

  1. 1
    Serwer gry→Klient gry

    Gracz atakuje lub łowi. Serwer wstrzymuje akcję i daje klientowi token podpisany twoim tajnym kluczem: który gracz, kiedy i jednorazowy kod.

  2. 2
    Klient gry→Usługa captcha

    Klient wysyła ten token do usługi i prosi o pytanie. Usługa sprawdza podpis, więc pytania dostają tylko żądania z twojego serwera.

  3. 3
    Usługa captcha→Klient gry

    Usługa wysyła obraz, pytanie i identyfikator pytania.

  4. 4
    Klient gry→Usługa captcha

    Gracz wybiera pasujące zwierzęta. Klient wysyła kliknięcia razem z identyfikatorem pytania.

  5. 5
    Usługa captcha→Klient gry

    Dobrze: usługa wysyła token przepustki podpisany twoim tajnym kluczem, z tym samym jednorazowym kodem. Źle: gracz czeka kilka sekund, a klient pyta ponownie tym samym tokenem.

  6. 6
    Klient gry→Serwer gry

    Klient wysyła token przepustki do serwera. Serwer sprawdza podpis twoim tajnym kluczem i czy jednorazowy kod pochodzi od niego, po czym pozwala na akcję. Serwer nigdy nie łączy się z usługą.

Sterowane z panelu

  • Włączaj i wyłączaj ochronę bez aktualizacji serwera
  • Ustal, ile zadań gracz musi rozwiązać w danym przedziale poziomów
  • Wyłącz sprawdzanie powyżej wybranego poziomu
  • Pytania w języku gracza, domyślnie po angielsku

Przykładowa instalacja Metin2

Korzysta z Crypto++ i rapidjson, które są już w źródłach, więc nie potrzeba dodatkowej biblioteki.

Pobierz pliki z panelu

Pliki na tej stronie to szablony. Kopie w panelu mają już wpisany adres API, ID strony i sekret, więc każdy serwer dostaje własne pliki. Nie udostępniaj plików serwera: każdy, kto ma zapisany w nich sekret, może tworzyć ważne tokeny captcha dla twojego serwera. Pliki klienta zawierają tylko adres API.

Klient (interfejs)

  1. 1

    Dodaj do strony thick_captcha.js z panelu.

    thick_captcha.js

    <script src="thick_captcha.js"></script>
  2. 2

    Otwórz go z tokenem z serwera gry i odeślij token przejścia do serwera.

    <div id="captcha"></div>
    <script>
      ThickCaptcha.open(document.getElementById("captcha"), {
        token: REQUEST_TOKEN_FROM_GAME_SERVER,
        lang: navigator.language,
        onPass: function (passToken) {
          sendToGameServer("/thick_captcha " + passToken);
        },
        onRenew: function () {
          return askGameServerForNewToken();
        },
      });
    </script>

Serwer gry

  1. 1

    Włącz system tym define. Bez tej linii system jest całkowicie wyłączony.

    common/service.h (CommonDefines.h)

    #define ENABLE_THICK_CAPTCHA
  2. 2

    Skopiuj thick_captcha.h i thick_captcha.cpp z panelu do game/src i dodaj .cpp do Makefile.

    game/src/Makefile

    CPPFILE = ... thick_captcha.cpp
  3. 3

    Utwórz menedżera w main.cpp.

    game/src/main.cpp

    #ifdef ENABLE_THICK_CAPTCHA
    #include "thick_captcha.h"
    #endif
    
    #ifdef ENABLE_THICK_CAPTCHA
    	CThickCaptchaManager thick_captcha_manager;
    #endif
  4. 4

    Ustaw CONFIG_PATH tak, aby wszystkie core wskazywały ten sam plik. Komendy GM zapisują tam ustawienia, a każdy core wczytuje plik ponownie raz na minutę, więc zmiana na jednym kanale trafia na wszystkie kanały i przetrwa restart. Plik tworzy się przy pierwszym uruchomieniu.

    game/src/thick_captcha.cpp, thick_captcha.txt

    const char*	CONFIG_PATH = "../../thick_captcha.txt";
    
    // thick_captcha.txt
    enabled 1
    difficulty 1
    interval 1 30
    interval 75 60
    interval 120 0
  5. 5

    Pytanie pojawia się przy pierwszym ataku na potwora lub metin i pierwszym łowieniu po zalogowaniu, a potem po upływie czasu dla poziomu gracza (domyślnie 30 minut, ustawiane przez thick_captcha_interval). Ataki na graczy (wojny, wojny gildii, PvP) i bezczynni gracze nigdy nie wywołują pytania.

    game/src/char_battle.cpp, char_skill.cpp, char.cpp

    // CHARACTER::Attack, after battle_is_attackable
    #ifdef ENABLE_THICK_CAPTCHA
    	if (IsPC() && !CThickCaptchaManager::instance().CheckAttack(this, pkVictim))
    		return false;
    #endif
    
    // CHARACTER::ComputeSkill, at the top
    #ifdef ENABLE_THICK_CAPTCHA
    	if (IsPC() && pkVictim && !CThickCaptchaManager::instance().CheckAttack(this, pkVictim))
    		return BATTLE_NONE;
    #endif
    
    // CHARACTER::fishing, after the "if (m_pkFishingEvent) { fishing_take(); return; }" block
    #ifdef ENABLE_THICK_CAPTCHA
    	if (!CThickCaptchaManager::instance().CheckAction(this))
    		return;
    #endif
  6. 6

    Wyczyść dane, gdy gracz się rozłącza. Oczekująca captcha zostanie wysłana ponownie przy następnym ataku lub łowieniu po ponownym zalogowaniu.

    game/src/char.cpp

    // CHARACTER::Disconnect
    #ifdef ENABLE_THICK_CAPTCHA
    	CThickCaptchaManager::instance().OnLogout(this);
    #endif
  7. 7

    Dodaj komendy gracza i komendy GM. thick_captcha_ask wysyła testową captcha, thick_captcha_toggle włącza lub wyłącza system, thick_captcha_difficulty 1-10 ustawia trudność, thick_captcha_interval <poziom> <minuty> ustala, jak często pytani są gracze od tego poziomu (0 = nigdy, del <poziom> usuwa regułę, bez argumentów wyświetla reguły). Zmiany zapisują się w pliku konfiguracyjnym i w ciągu minuty trafiają na wszystkie kanały.

    game/src/cmd.cpp, cmd_general.cpp

    #ifdef ENABLE_THICK_CAPTCHA
    ACMD(do_thick_captcha);
    ACMD(do_thick_captcha_renew);
    ACMD(do_thick_captcha_ask);
    ACMD(do_thick_captcha_toggle);
    ACMD(do_thick_captcha_difficulty);
    ACMD(do_thick_captcha_interval);
    #endif
    
    #ifdef ENABLE_THICK_CAPTCHA
    	{ "thick_captcha",       do_thick_captcha,       0, POS_DEAD, GM_PLAYER     },
    	{ "thick_captcha_renew", do_thick_captcha_renew, 0, POS_DEAD, GM_PLAYER     },
    	{ "thick_captcha_ask",   do_thick_captcha_ask,   0, POS_DEAD, GM_LOW_WIZARD },
    	{ "thick_captcha_toggle",     do_thick_captcha_toggle,     0, POS_DEAD, GM_IMPLEMENTOR },
    	{ "thick_captcha_difficulty", do_thick_captcha_difficulty, 0, POS_DEAD, GM_IMPLEMENTOR },
    	{ "thick_captcha_interval",   do_thick_captcha_interval,   0, POS_DEAD, GM_IMPLEMENTOR },
    #endif
    
    #ifdef ENABLE_THICK_CAPTCHA
    ACMD(do_thick_captcha)
    {
    	while (*argument == ' ')
    		++argument;
    	if (!CThickCaptchaManager::instance().Verify(ch, argument))
    		ch->ChatPacket(CHAT_TYPE_COMMAND, "ThickCaptchaFailed");
    }
    
    ACMD(do_thick_captcha_renew)
    {
    	CThickCaptchaManager::instance().Renew(ch);
    }
    
    ACMD(do_thick_captcha_ask)
    {
    	char arg1[256];
    	one_argument(argument, arg1, sizeof(arg1));
    	LPCHARACTER tch = *arg1 ? CHARACTER_MANAGER::instance().FindPC(arg1) : ch;
    	if (tch)
    		CThickCaptchaManager::instance().Request(tch);
    }
    
    ACMD(do_thick_captcha_toggle)
    {
    	CThickCaptchaManager& mgr = CThickCaptchaManager::instance();
    	mgr.SetEnabled(!mgr.IsEnabled());
    	ch->ChatPacket(CHAT_TYPE_INFO, "Thick Captcha: %s", mgr.IsEnabled() ? "on" : "off");
    }
    
    ACMD(do_thick_captcha_difficulty)
    {
    	char arg1[256];
    	one_argument(argument, arg1, sizeof(arg1));
    	CThickCaptchaManager& mgr = CThickCaptchaManager::instance();
    	if (*arg1 && isdigit(*arg1))
    		mgr.SetDifficulty(atoi(arg1));
    	ch->ChatPacket(CHAT_TYPE_INFO, "Thick Captcha difficulty: %d (1-10)", mgr.GetDifficulty());
    }
    
    ACMD(do_thick_captcha_interval)
    {
    	char arg1[256], arg2[256];
    	two_arguments(argument, arg1, sizeof(arg1), arg2, sizeof(arg2));
    	CThickCaptchaManager& mgr = CThickCaptchaManager::instance();
    
    	if (!strcmp(arg1, "del") && isdigit(*arg2))
    		mgr.RemoveInterval(atoi(arg2));
    	else if (isdigit(*arg1) && isdigit(*arg2))
    		mgr.SetInterval(atoi(arg1), atoi(arg2));
    
    	for (const auto& rule : mgr.GetIntervals())
    	{
    		if (rule.second)
    			ch->ChatPacket(CHAT_TYPE_INFO, "Thick Captcha level %d+: every %d min", rule.first, rule.second);
    		else
    			ch->ChatPacket(CHAT_TYPE_INFO, "Thick Captcha level %d+: off", rule.first);
    	}
    }
    #endif
  8. 8

    Aby chronić inną akcję, dodaj tam to samo sprawdzenie.

    #ifdef ENABLE_THICK_CAPTCHA
    	if (!CThickCaptchaManager::instance().CheckAction(ch))
    		return;
    #endif