Eingehende + Ausgehende Packets abfangen

Hier könnt ihr anderen Leuten helfen, indem ihr Anleitungen oder praktische Codesegmente zur Verfügung stellt.

Eingehende + Ausgehende Packets abfangen

Beitragvon Aquaatic » So 3. Mai 2015, 12:39

Hey!

ACHTUNG: Dieses Tutorial ist NICHT für Anfänger gedacht.

Achtung: Dies dieses Tutorial enhält komplizierte Satzstrukturen mit vielen Nebensätzen und Objekten, um es so kurz wie möglich zu halten. Also: Am besten 2x lesen :)

Ebenfalls beleuchte ich hier lediglich das Prinzip! Weiteres soll von euch gemacht werden. Ich werde am Ende auch eine kleine Aufgabe geben.

Vorraussetzungen (darauf werde ich nicht weiter eingehen):
  • Grundlegende Kenntnisse mit der Bukkit-/Spigot API
  • Gute Java Kenntnisse (Syntax, Interfaces, ...)
  • Reflection Kentnisse
  • Kenntnisse zu Packets (Bukkit/Spigot) - Nur für Teil 2
  • Objektorientierung - Nur für Teil 2

Teile:
1. Eingehende Packets abfangen (PacketPlayInUseEntity)
2. Ausgehende Packets abfangen (PacketPlayOutExplosion)

Teil 1: Eingehende Packets abfangen

Das Prinzip:
Erarbeiten wir uns zunächst den Weg, das ein eingehendes Packet zurücklegt an dem Beispiel des UseEntity Packets(Rechtsklick/Linksklick auf Entity).
  1. Spieler schlägt ein Entity
  2. Spieler sendet das Packet an den Server (Internet, klar? :D)
  3. Server: Oh toll! Ein neues Packet. Geben wir das mal an unsere IO Software (bei Bukkit/Spigot Netty) weiter!
  4. Netty findet das Packet auf und durchläuft folgende "Stadien" (Pipeline-Prinzip):
    1. timeout
    2. legacy_query
    3. splitter
    4. decoder -> Wird interessant für uns
    5. prepender
    6. encoder
    7. packet_handler
  5. Bukkit/Spigot bearbeitet das Packet

Jetzt müssen wir uns irgendwo einhaken. Das mache ich meißtens nach dem "decoder".
D.h Wir warten bis ein Packet es bis zum "decoder" in der Pipeline schafft und bearbeiten es dann. Dazu fügen wir nach dem "decoder" einen sogenannten "ChannelHandler" ein.

So genug langweiliges Prinzip. Auf zum Praktischen.
Die Pipeline wird in Netty durch das Interface "ChannelPipeline" representiert. Eine Instanz davon bekommen wir durch die im NetworkManager befindliche Channel-Instanz mit der Methode "pipeline()". Der NetworkManager befindet sich in der PlayerConnection der EntityPlayer Instanz des Spielers, die sich durch die Methode "getHandle()" in der Klasse CraftPlayer bekommen lässt.

Die Channel-Instanz ist dabei in einem privaten Feld gespeichert. Daher müssen wir Reflection verwenden, um diese zu bekommen.
Dieses Field werden wir statisch speichern, um es dann immer wieder aufrufen zu können.

Dann werden wir im Statischen Initialisierungsblock, welcher bei erstem Aufruf der Klasse aufgerufen wird, die Variable initialisieren. Dazu iterieren wir durch alle Felder der NetworkManager-Klasse und überprüfen, ob sie von der Channel-Klasse bestimmbar ist.

Also:
Code: Alles auswählen
  1. private static Field channelField;
  2.    
  3. static {
  4.     for(Field f : NetworkManager.class.getDeclaredFields()) { //Iterieren
  5.         if(f.getType().isAssignableFrom(Channel.class)) { //Wenn es ein Channel ist
  6.             channelField = f; //Field-Variable initialisieren
  7.             channelField.setAccessible(true); //Schreib-/lesbar machen
  8.             break; //Ende
  9.         }
  10.     }
  11. }


Erstellen wir uns nun das Interface "PacketReceivingHandler", welches eine Methode verlangen wird, die ein Packet und eine Player-Instanz verlangt, die dann aufgerufen wird, wenn das Packet angekommen ist.
Ebenfalls muss ein "boolean" gereturnt werden, welcher anzeigt, ob das Packet weitergesendet werden soll:
Code: Alles auswählen
  1. public static interface PacketReceivingHandler {
  2.     public boolean handle(Player p, PacketPlayInUseEntity packet);
  3. }


Nun haben wir unser Channel-Field. Erstellen wir nun den Methodenbody, der eine Player-Instanz und eine Instanz des PacketReceivingHandler Interfaces.
Dann noch dazu eine Methode um den Netty-Channel zu bekommen.
Code: Alles auswählen
  1. public ChannelHandler listen(Player p, PacketReceivingHandler handler) {
  2.     Channel ch = getNettyChannel(p); //Netty-Channel
  3.     ChannelPipeline pipe = ch.pipeline(); //Oben erwähnte pipeline() Methode aufrufen, um auf die ChannelPipeline zuzugreifen
  4. }
  5.    
  6. public Channel getNettyChannel(Player p) {
  7.     NetworkManager manager = ((CraftPlayer)p).getHandle().playerConnection.networkManager;
  8.     Channel channel = null;
  9.     try {
  10.         channel = (Channel) channelField.get(manager);
  11.     } catch (IllegalArgumentException | IllegalAccessException e) {
  12.         e.printStackTrace();
  13.     }
  14.     return channel;
  15. }


Nun können wir über die Methode ChannelPipeline#addAfter(String baseName, String name, ChannelHandler handler) unseren "Listener" einfügen. Dazu müssen wir eine ChannelHandler-Objekt übergeben. Als ChannelHandler verwenden wir einen MessageToMessageDecoder, der ChannelHandler implementiert. Dieser benötigt ein Generic, für das wir Packet (aus net.minecraft.server.[VERSION]) einsetzten.
Da es eine Abstrakte Klasse ist müssen/können wir eine innere anonyme Klasse erstellen (klar, eine normale tuts auch).
Code: Alles auswählen
  1. ChannelHandler handle = new MessageToMessageDecoder<Packet>() {
  2.     @Override
  3.         protected void decode(ChannelHandlerContext chc, Packet packet, List<Object> out) throws Exception {
  4.             //...
  5.         }
  6.     };
  7. pipe.addAfter("decoder", //Wie oben erwähnt nach dem decoder
  8.         "listener", //Channel Name -> DARF NICHT DOPPELT VORKOMMEN!
  9.         handle); //Unser zuvor erstelltes ChannelHandler Objekt
  10. return handle; //Zum schließen des Listeners


Nun wird immer wenn ein Packet empfangen wird die "decode" Methode aufgerufen. Um das Packet auf seinen Weg weiterzuschicken fügen wir es zu der out-Liste hinzu. Wenn nicht dann nicht.
Wenn die Methode jetzt ankommt überprüfen wir, ob das packet eine Instanz von PacketPlayInUseEntity ist. Wenn ja rufen wir die "handle" Methode im "PacketReceivingHandler" auf und leiten das Packet je nach dem, was es zurückgibt weiter oder nicht.
Code: Alles auswählen
  1. if(packet instanceof PacketPlayInUseEntity) {
  2.     if(!handler.handle(p, (PacketPlayInUseEntity) packet)) { //Wenn es false zurückgibt (wenn es weitergeleitet werden soll)
  3.         out.add(packet); //weiterleiten
  4.     }
  5.     return; //Fertig
  6. }
  7. out.add(packet); //Wenn es keine Instanz ist trotzdem weiterleiten, sonst würde nichts mehr auf dem Server laufen.


So. Wie jetzt der ein oder andere gemerkt hat returnen wir den ChannelHandler bei der listen Methode. Warum? Ganz einfach: Zum schließen:
Code: Alles auswählen
  1. /**
  2. * @return Erfolg
  3. */
  4. public boolean close(Player p, ChannelHandler handler) {
  5.     try {
  6.         ChannelPipeline pipe = getNettyChannel(p).pipeline();
  7.         pipe.remove(handler);
  8.         return true;
  9.     } catch(Exception e) {
  10.         return false;
  11.     }
  12. }


So. Das wäre es jeztz dafür. Zur Anwendung einfach z.B folgendes:
Code: Alles auswählen
  1. private ChannelHandler handler;
  2.    
  3. @EventHandler
  4. public void onJoin(PlayerJoinEvent e) {
  5.     handler = listen(e.getPlayer(), new PacketReceivingHandler() {
  6.         @Override
  7.         public boolean handle(Player p, PacketPlayInUseEntity packet) {
  8.             p.sendMessage("§7Du hast ein Entity §aangeklickt");
  9.             close(p, handler);
  10.             return false;
  11.         }
  12.     });
  13. }

/-\
| |
| |
Bitte den Code nicht verwenden, ist nur für 1 Spieler Server gedacht :D Lieber in eine HashMap<String, ChannelHandler> speichern.

Jezt nochmal die ganze Methode:
Code: Alles auswählen
  1. public ChannelHandler listen(final Player p, final PacketReceivingHandler handler) {
  2.     Channel ch = getNettyChannel(p);
  3.     ChannelPipeline pipe = ch.pipeline();
  4.     ChannelHandler handle = new MessageToMessageDecoder<Packet>() {
  5.         @Override
  6.         protected void decode(ChannelHandlerContext chc, Packet packet,
  7.                 List<Object> out) throws Exception {
  8.             if(packet instanceof PacketPlayInUseEntity) {
  9.                 if(!handler.handle(p, (PacketPlayInUseEntity) packet)) { //Wenn es false zurückgibt (wenn es weitergeleitet werden soll)
  10.                     out.add(packet); //weiterleiten
  11.                 }
  12.                 return; //Fertig
  13.             }
  14.             out.add(packet); //Wenn es keine Instanz ist trotzdem weiterleiten, sonst würde nichts mehr auf dem Server laufen.
  15.         }
  16.     };
  17.     pipe.addAfter("decoder", //Wie oben erwähnt nach dem decoder
  18.                 "listener", //Channel Name -> DARF NICHT DOPPELT VORKOMMEN!
  19.                 handle); //Unser zuvor erstelltes ChannelHandler Objekt
  20.     return handle; //Zum schließen des Listeners
  21. }


-> Das PacketPlayInUseEntity ist nur ein Beispiel. Es kann jedes anderes verwendet werden. Alle Packets sind [url='http://wiki.vg/Protocol#Explosion']hier[/url]aufgelistet.

Teil 2: Ausgehende Packets abfangen - Some Credits to @Janhektor
Mal wieder erstmal das Prinzip:
Wie man wissen sollte für diesen Teil sendet man die Packets über die PlayerConnection#sendPacket Methode. Diese verwendet ebenfalls Spigot. Man kann es ahnen:
Ich will diese Methode überschreiben. Aber wie macht man das am besten? PlayerConnection ist eine Klasse, die (wie man wissen sollte) eine andere Klasse erben kann mit all ihren Methoden.
Und genau das machen wir uns zu nutzen:
Vorgehensweise:
  1. Neue Klasse, die von PlayerConnection erbt
  2. Super-Constructor aufrufen
  3. Eigenen einfach zu benutzenden Konstrukor erstellen
  4. "sendPacket" Methode überschreiben
  5. Listener einfügen

1. -> Neue Klasse, die von PlayerConnection erbt
Man erstelle eine neue Klasse (respekt, wenn du nicht weißt, wie das geht, aber trotzdem bis hier gelesen hast :D) und lasse sie von PlayerConnection erben. (extends PlayerConnection)
Jetzt wird man relativ schnell merken, das Eclipse/andere IDE meckert.

2. -> Super-Constructor aufrufen
Code: Alles auswählen
  1. public CustomPlayerConnection(MinecraftServer minecraftserver,
  2.             NetworkManager networkmanager, EntityPlayer entityplayer) {
  3.     super(minecraftserver, networkmanager, entityplayer);
  4. }

-> Ich denke mal, dazu muss ich nichts erklären.

3. Eigenen einfach zu benutzenden Konstrukor erstellen
Zunächst erstellen wir einen Konstrukor, der einen EntityPlayer fordert. Diser beinhaltet die Fields
  • server -> MinecraftServer Instanz
  • playerConnection.networkManager -> NetworkManager Instanz
Also instanziieren wir unseren zuerst erstellten Konstrukor mit diesen Feldern und dem EntityPlayer selbst.
Code: Alles auswählen
  1. public CustomPlayerConnection(EntityPlayer p) {
  2.     this(p.server, p.playerConnection.networkManager, p);
  3. }

Nun... jetzt ist das ja immer noch nicht so schön.
Also erstellen wir uns eine Methode, die die EntityPlayer Instanz der Player-Instanz zurückgibt und erstellen einen weiteren Konstrukor mit dem Argument "Player", mit dem wir dann den zuvor erstellten Konstrukor instanziieren.
Code: Alles auswählen
  1. public CustomPlayerConnection(Player player) {
  2.     this(getNMSPlayer(player));
  3. }
  4. public static EntityPlayer getNMSPlayer(Player p) {
  5.     return ((CraftPlayer)p).getHandle();
  6. }


Das war es jetzt mit den Konstrukoren.

4. -> "sendPacket" Methode überschreiben
Folgender Code:
Code: Alles auswählen
  1. @Override
  2. public void sendPacket(Packet packet) {
  3.     super.sendPacket(packet); //Methode in PlayerConnection aufrufen
  4. }

- macht erstmal garnichts.

5. -> Listener einfügen
So hier passiert nun die Magie.
Wir erstellen ein neues Interface (mal wieder) mit dem Namen "PacketSendHandler" (oder auch anders)
Code: Alles auswählen
  1. public static interface PacketSendHandler {
  2.     public boolean handle(Player p, PacketPlayOutExplosion packet);
  3. }

Wie man jetzt unschwer erkennen kann fange ich hier beispielweise das PacketPlayOutExplosion ab.
Nun erstelle ich ein neues Feld in der Klasse, die von PlayerConnection erbt (in meinem Fall "CustomPlayerConnection") mit dem Typ PacketSendHandler und auch noch einen Setter dafür.
Code: Alles auswählen
  1. private PacketSendHandler handler;
  2. public void setHandler(PacketSendHandler handler) {
  3.     this.handler = handler;
  4. }


So nun fügen wir in der "sendPacket" Methode eine if-Abfrage ein, die überprüft, ob der Handler schon gesetzt wurde.
Wenn ja schauen wir, ob das Packet eine Instanz von "PacketPlayOutExplosion" ist. Wenn ja geben wir das an den Handler weiter, der dann wieder entscheidet, ob es weitergeleitet werden soll. (true wenn nicht)
Wenn es nicht weitergeleitet werden soll returnt man einfach, um die Methode in der Super-Klasse nicht aufzurufen.
Code: Alles auswählen
  1. if(handler != null) {
  2.     if(packet instanceof PacketPlayOutExplosion) { //Wenn es eine Instanz ist
  3.         if(handler.handle((Player) this.player.getBukkitEntity(), //getBukkitEntity returnt einen CraftPlayer, der einfach zum Player gecastet werden kann.
  4.             (PacketPlayOutExplosion) packet)) { //Lass den Handler regeln
  5.             return; //Wenn nicht weitergeleitet werden soll return
  6.         }
  7.     }
  8. }


Nun bringt uns das erstmal überhaupt nichts. Wir müssen die PlayerConnection des Spielers erst beim Join mit unserer CustomPlayerConnection initialisieren.
Also:
Code: Alles auswählen
  1. CustomPlayerConnection pc = new CustomPlayerConnection(e.getPlayer());
  2. CustomPlayerConnection.getNMSPlayer(e.getPlayer()).playerConnection = pc;


Dann setzen wir noch den Handler, der in meinem Fall dem Spieler eine Nachicht sendet.
Code: Alles auswählen
  1. pc.setHandler(new PacketSendHandler() {
  2.     @Override
  3.     public boolean handle(Player p, PacketPlayOutExplosion packet) {
  4.         p.sendMessage("§cBOOM!");
  5.         return false;
  6.     }
  7. });


Das ganze sieht dann z.B. so aus:
Code: Alles auswählen
  1. @EventHandler
  2. public void onJoin(PlayerJoinEvent e) {
  3.     CustomPlayerConnection pc = new CustomPlayerConnection(e.getPlayer());
  4.     CustomPlayerConnection.getNMSPlayer(e.getPlayer()).playerConnection = pc;
  5.     pc.setHandler(new PacketSendHandler() {
  6.         @Override
  7.         public boolean handle(Player p, PacketPlayOutExplosion packet) {
  8.             p.sendMessage("§cBOOM!");
  9.             return false;
  10.         }
  11.     });
  12. }


Event registrieren nicht vergessen ;)

Aufgabe: Schreibe einen Packetunabhängigen PacketListener (am besten gibt man irgendwo das Packet an). Sende es per P/H-astebin hier drunter oder an mein Skype: niklas-5999 - Ist ein Experiment

Soo, das wars!
Bei Fehlern bitte melden, Kritik oder Verbesserungsvorschläge sehr erwünscht.
Zuletzt geändert von Aquaatic am Fr 8. Mai 2015, 20:18, insgesamt 1-mal geändert.
Mit freundlichen Grüßen
~ Aquaatic
Benutzeravatar
Aquaatic
 
Beiträge: 148
Registriert: Mo 16. Feb 2015, 12:51

Re: Eingehende + Ausgehende Packets abfangen

Beitragvon Jofkos » So 3. Mai 2015, 12:49

Ich würde ausgehende Packets auch über Netty abfangen, ansonsten sieht das ganz gut aus :)
Jofkos

...........

..Bild
Benutzeravatar
Jofkos
 
Beiträge: 1537
Registriert: So 16. Jun 2013, 22:45

Re: Eingehende + Ausgehende Packets abfangen

Beitragvon Aquaatic » So 3. Mai 2015, 12:53

Joa die Idee mit der CustomPlayerConnection hatte Janhektor :D
Mit freundlichen Grüßen
~ Aquaatic
Benutzeravatar
Aquaatic
 
Beiträge: 148
Registriert: Mo 16. Feb 2015, 12:51

Re: Eingehende + Ausgehende Packets abfangen

Beitragvon Sep2703 » So 3. Mai 2015, 14:13

Aquaatic hat geschrieben:Joa die Idee mit der CustomPlayerConnection hatte Janhektor :D

Ja.
Ich würde mich für die Netty-Lösung mal interessieren, @Jofkos. Hast du da zufällig eine Idee?
Du möchtest programmieren lernen oder dein Bukkit-/Spigot-Wissen erweitern?
Hier habe ich für dich kostenlose Tutorials: https://youtube.com/janhektor
Benutzeravatar
Sep2703
 
Beiträge: 677
Registriert: Mi 8. Jan 2014, 15:13
Wohnort: 127.0.0.1

Re: Eingehende + Ausgehende Packets abfangen

Beitragvon Jofkos » So 3. Mai 2015, 16:24

Ich hab' die Packets einfach "nach" dem Encoder abgefangen. Die Pipeline geht in beide Richtungen (siehe hier), in diesem Fall ist danach halt davor
Code: Alles auswählen
  1.    @EventHandler
  2.    public void onJoin(PlayerJoinEvent e) {
  3.       EntityPlayer entity = ((CraftPlayer) e.getPlayer()).getHandle();
  4.       Channel playerChannel = Reflect.get(channelField, entity.playerConnection.networkManager);
  5.       
  6.       playerChannel.pipeline().addAfter("encoder", "out_listener", new MessageToMessageEncoder<Packet>() {
  7.          
  8.          @Override
  9.          protected void encode(ChannelHandlerContext chc, Packet packet, List<Object> out) throws Exception {
  10.             //irgendwass mit packet machen
  11.             out.add(packet); // Packet weitergeben
  12.          }
  13.          
  14.       });
  15.    }


//Edit: Hatte die Lösung hier schonmal gepostet.
Jofkos

...........

..Bild
Benutzeravatar
Jofkos
 
Beiträge: 1537
Registriert: So 16. Jun 2013, 22:45

Re: Eingehende + Ausgehende Packets abfangen

Beitragvon Twister_21 » Mo 15. Jun 2015, 15:56

Hallo.
Das gehört zwar nicht ganz dazu, aber wie kann man es schaffen, dass man Packets manuell senden kann. Ich will das mit dem PacketPlayOutPlayerInfo machen. Das bekommt man von allen Spielern zugesendet, wenn man den Server beitritt. Nun will ich es aber so machen, dass ich wie z.B. bei der TagAPI einen Spieler "refreshen" kann und dann seine PlayerInfoDaten in PlayerInfo-Packets an alle Spieler noch mal gesendet werden. Dann kann ich diese wieder modifizieren und weitergeben.
Mit freundlichen Grüßen
Twister21
Benutzeravatar
Twister_21
 
Beiträge: 652
Registriert: Mi 11. Jun 2014, 05:51
Wohnort: Deutschland


Zurück zu Anleitungen

Wer ist online?

Mitglieder in diesem Forum: 0 Mitglieder und 3 Gäste