Bot-Schutz für Spiele

Spieler klicken ein paar Bilder an. Bots bleiben hängen. Dein Gameserver signiert und prüft jedes Token selbst, deshalb ändert sich bei dir nichts, wenn du die Regeln anpasst.

Vorschau & Einrichtung

Probier es aus

Eine Beispielfrage. Wähle alle passenden Tiere und drücke dann Weiter.

So funktioniert es

  1. 1
    Gameserver→Spiel-Client

    Ein Spieler greift an oder angelt. Der Server hält die Aktion an und gibt dem Client ein mit deinem geheimen Schlüssel signiertes Token: welcher Spieler, wann und ein Einmalcode.

  2. 2
    Spiel-Client→Captcha-Dienst

    Der Client sendet dieses Token an den Dienst und fragt nach einer Frage. Der Dienst prüft die Signatur, damit nur Anfragen von deinem Server Fragen bekommen.

  3. 3
    Captcha-Dienst→Spiel-Client

    Der Dienst sendet ein Bild, eine Frage und eine Fragen-ID.

  4. 4
    Spiel-Client→Captcha-Dienst

    Der Spieler wählt die passenden Tiere. Der Client sendet die Klicks zusammen mit der Fragen-ID.

  5. 5
    Captcha-Dienst→Spiel-Client

    Richtig: Der Dienst sendet ein mit deinem geheimen Schlüssel signiertes Pass-Token mit demselben Einmalcode. Falsch: Der Spieler wartet ein paar Sekunden, dann fragt der Client mit demselben Token erneut.

  6. 6
    Spiel-Client→Gameserver

    Der Client sendet das Pass-Token an den Server. Der Server prüft die Signatur mit deinem geheimen Schlüssel und ob der Einmalcode von ihm stammt, dann erlaubt er die Aktion. Der Server ruft den Dienst nie auf.

Über das Panel gesteuert

  • Bot-Prüfung ein- und ausschalten, ohne den Server zu aktualisieren
  • Festlegen, wie viele Aufgaben ein Spieler je Levelbereich lösen muss
  • Prüfung ab einem bestimmten Level abschalten
  • Fragen in der Sprache des Spielers, standardmäßig Englisch

Metin2-Beispiel

Nutzt Crypto++ und rapidjson, die bereits im Source enthalten sind, also keine zusätzliche Bibliothek.

Lade die Dateien aus deinem Panel

Die Dateien auf dieser Seite sind Vorlagen. Die Kopien in deinem Panel enthalten bereits deine API-Adresse, Site-ID und dein Secret, jeder Server bekommt also eigene Dateien. Halte die Server-Dateien geheim: Wer das Secret darin hat, kann gültige Captcha-Tokens für deinen Server erzeugen. Die Client-Dateien enthalten nur die API-Adresse.

Client (UI)

  1. 1

    Binde thick_captcha.js aus deinem Panel in deine Seite ein.

    thick_captcha.js

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

    Öffne es mit dem Token deines Gameservers und schicke das Pass-Token an den Server zurück.

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

Gameserver

  1. 1

    Schalte das System mit diesem Define ein. Ohne die Zeile ist es komplett aus.

    common/service.h (CommonDefines.h)

    #define ENABLE_THICK_CAPTCHA
  2. 2

    Kopiere thick_captcha.h und thick_captcha.cpp aus deinem Panel nach game/src und trage die .cpp im Makefile ein.

    game/src/Makefile

    CPPFILE = ... thick_captcha.cpp
  3. 3

    Erstelle den Manager in 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

    Setze CONFIG_PATH so, dass alle Cores auf dieselbe Datei zeigen. GM-Befehle speichern ihre Einstellungen dort und jeder Core liest die Datei einmal pro Minute neu, so erreicht eine Änderung auf einem Channel alle Channels und übersteht Neustarts. Die Datei wird beim ersten Start angelegt.

    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

    Gefragt wird beim ersten Angriff auf ein Monster oder einen Metin und beim ersten Angeln nach dem Login, danach wieder nach dem Intervall für das Level des Spielers (standardmäßig 30 Minuten, einstellbar mit thick_captcha_interval). Angriffe auf Spieler (Kriege, Gildenkriege, PvP) und untätige Spieler lösen nie eine Frage aus.

    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

    Räume beim Ausloggen des Spielers auf. Ein offenes Captcha wird nach dem erneuten Einloggen beim nächsten Angriff oder Angeln wieder gesendet.

    game/src/char.cpp

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

    Füge die Spielerbefehle und die GM-Befehle hinzu. thick_captcha_ask sendet ein Test-Captcha, thick_captcha_toggle schaltet das System ein oder aus, thick_captcha_difficulty 1-10 setzt die Schwierigkeit, thick_captcha_interval <Level> <Minuten> legt fest, wie oft Spieler ab diesem Level gefragt werden (0 = nie, del <Level> löscht eine Regel, ohne Argumente werden die Regeln angezeigt). Änderungen werden in der Config-Datei gespeichert und erreichen alle Channels innerhalb einer Minute.

    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

    Um eine weitere Aktion zu schützen, füge dort dieselbe Prüfung ein.

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