Let external apps communicate with server

Discussion in 'Plugin Development' started by knokko, Dec 9, 2018.

  1. Offline

    knokko

    I'm looking for a way to let external applications connect to the server that uses my plug-in and let those applications communicate with the plug-in. (Those external applications would be applications or minecraft mods that players can download and use that to do or view things on the server.)
    The most straightforward way would be to open a new ServerSocket and use that one, but that would require the host to open another port, which usually costs extra money or is not possible at all.

    That's why I am looking for a way to use the same ServerSocket that is being used by minecraft and then distinguish between normal player connections and special connections.

    This will most likely be a dirty job that requires reflection, but I would be ok with that. The problem is that I can't find where I should 'hack my way in'.

    Does anyone know where I should look?
    And I am also interested in alternative ways to accomplish this if anyone happens to have such an idea.
     
  2. Offline

    Shadow_tingBRO

    I tried doing this some time ago... It kinda worked; when i did a command which switched the host 'on' or connected it the server would lagggg until i wrote in a command to send, and then it would do it, the problem is it mixed up with bukkit's sockets so i'm not sure but may be look at some plugins that do do this and see how they work?
     
  3. Offline

    knokko

    @Shadow_tingBRO

    I haven't found any other plug-ins that do this. All plug-ins I have seen so far create a new server socket instead of using minecraft/bukkits serversocket.
    Could you please send me the codes you used to accomplish that a long time ago? (Or give a rough sketch of how you did that if you don't have those codes anymore). I might be able to finish your work.
     
  4. Online

    timtower Ninja on the waves Moderator

    @knokko Haven't seen any programs that can use the same port for different protocols yet.
    External applications can be tricky, but how external are we talking about? Same machine or different one that is half way around the world?
    Mods are possible within the protocol already, there is a custom packet somewhere.
     
  5. Offline

    knokko

    @timtower

    With external programs, I mean (java) applications that I write and let other people download. That application has something to do with the server (for instance, it could be an application that lets view scores, shows all kinds of info about your faction, shows long chat logs. can be useful since I could create proper GUI's instead of the inventory menus of Bukkit). That application uses a client socket to talk to the server. (Those are just examples, I can't think of better examples right now.)

    Websites would also be interesting this way, but that is going to be annoying since javascript TCP sockets are usually experimental or not supported at all.

    Mods should indeed be relatively easy since I could just let the player execute a command and let the server register it as a normal command.
     
  6. Offline

    knokko

    I have been trying a lot of things and I managed to make small progress. I can let an external application send a String to the server and I can let the server send a bytebuffer back. With a bit of clever coding, I could make this more or less stable, but there is 1 big problem:
    Once the server sends the response, the connection will be terminated by some other handler. Also, for some reason, receiving the message from the external application can only be done once after every server restart. So if my application needs to send another message to the server, I need to restart the server first...

    The plug-in code:
    Code:
    package nl.knokko.doubleserver.plugin;
    
    import java.lang.reflect.Field;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.logging.Level;
    
    import org.bukkit.Bukkit;
    import org.bukkit.command.Command;
    import org.bukkit.command.CommandExecutor;
    import org.bukkit.command.CommandSender;
    import org.bukkit.event.EventHandler;
    import org.bukkit.event.Listener;
    import org.bukkit.event.player.PlayerJoinEvent;
    import org.bukkit.plugin.java.JavaPlugin;
    
    import io.netty.buffer.ByteBuf;
    import net.minecraft.server.v1_12_R1.HandshakeListener;
    import net.minecraft.server.v1_12_R1.IChatBaseComponent;
    import net.minecraft.server.v1_12_R1.MinecraftServer;
    import net.minecraft.server.v1_12_R1.NetworkManager;
    import net.minecraft.server.v1_12_R1.PacketHandshakingInSetProtocol;
    import net.minecraft.server.v1_12_R1.PacketListener;
    import net.minecraft.server.v1_12_R1.PacketStatusInPing;
    import net.minecraft.server.v1_12_R1.PacketStatusInStart;
    import net.minecraft.server.v1_12_R1.PacketStatusListener;
    import net.minecraft.server.v1_12_R1.ServerConnection;
    
    public class ServerPlugin extends JavaPlugin implements Listener {
    
        private List<NetworkManager> networkManagers;
    
        @Override
        @SuppressWarnings("unchecked")
        public void onEnable() {
            getCommand("testnetworkmanagers").setExecutor(new TestExecutor());
            /*
             * Currently found packet listeners are: null - the packet listener is not set
             * before the network manager is added to the list HandshakeListener - the first
             * relevant packet listener PacketStatusListener - the packet listener that is
             * used for players that browse the server list LoginListener - the listener
             * that performs the logging in PlayerConnection - packet listener for players
             * that are logged in
             */
            @SuppressWarnings("deprecation")
            ServerConnection serverConnection = MinecraftServer.getServer().an();
            try {
                Field fieldH = ServerConnection.class.getDeclaredField("h");
                fieldH.setAccessible(true);
                networkManagers = (List<NetworkManager>) fieldH.get(serverConnection);
                networkManagers = new NetworkManagerList(networkManagers.size());
                System.out.println("networkManagers is " + networkManagers);
                fieldH.set(serverConnection, networkManagers);
                printNetworkManagers();
                Bukkit.getPluginManager().registerEvents(this, this);
            } catch (Exception ex) {
                Bukkit.getLogger().log(Level.SEVERE, "It looks like reflection went wrong", ex);
            }
        }
    
        @EventHandler
        public void onPlayerJoin(PlayerJoinEvent event) {
            printNetworkManagers();
            Bukkit.getScheduler().scheduleSyncDelayedTask(this, () -> {
                System.out.println("networkManagers became " + networkManagers);
                printNetworkManagers();
            });
        }
    
        void printNetworkManagers() {
            for (NetworkManager manager : networkManagers) {
                System.out.println("packet listener is now " + manager.i());
            }
        }
    
        private class TestExecutor implements CommandExecutor {
    
            @Override
            public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
                printNetworkManagers();
                return false;
            }
    
        }
    
        private class NetworkManagerList extends ArrayList<NetworkManager> {
    
            private static final long serialVersionUID = 2536735564832299451L;
    
            private NetworkManagerList(int capacity) {
                super(capacity);
            }
    
            @Override
            @SuppressWarnings("deprecation")
            public boolean add(NetworkManager manager) {
                System.out.println("Old packet listener is " + manager.i());
                try {
                    Bukkit.getScheduler().scheduleSyncDelayedTask(ServerPlugin.this, () -> {
                        PacketListener oldListener = manager.i();
                        System.out.println("The packet listener changed to " + oldListener + " in the meantime");
                        if (oldListener instanceof HandshakeListener)
                            manager.setPacketListener(new CustomHandshakeListener(MinecraftServer.getServer(), manager));
                        else if (oldListener instanceof PacketStatusListener)
                            manager.setPacketListener(new CustomPacketStatusListener(MinecraftServer.getServer(), manager));
                        else
                            System.out.println("Didn't change packet listener");
                    });
                    return super.add(manager);
                } catch (Exception e) {
                    throw new Error("Something went wrong with reflection", e);
                }
            }
        }
    
        // static final HashMap<InetAddress, Long> throttleTracker = new HashMap<InetAddress, Long>();
        //private static int throttleCounter = 0;
    
        private class CustomHandshakeListener extends HandshakeListener {
           
            private final NetworkManager b;
    
            public CustomHandshakeListener(MinecraftServer minecraftserver, NetworkManager networkmanager) {
                super(minecraftserver, networkmanager);
                this.b = networkmanager;
            }
    
            @Override
            public void a(IChatBaseComponent arg0) {
                System.out.println("CustomHandshakeListener.a(IChatBaseComponent): " + arg0.getText());
                ByteBuf testBuffer = b.channel.alloc().buffer(10);
                testBuffer.writeBytes(new byte[] {1,2,3,4,5,6,7,8,9,10});
                b.channel.writeAndFlush(testBuffer);
                super.a(arg0);
            }
    
            private static final int CUSTOM_PROTOCOL_VERSION = 15;
    
            @Override
            public void a(PacketHandshakingInSetProtocol p) {
                System.out.println("CustomHandshakeListener.a(PacketHandshakingInSetProtocol)");
                System.out.println(p.hostname + ":" + p.port);
                System.out.println(p.a() + " " + p.b());
                if (p.b() == CUSTOM_PROTOCOL_VERSION) {
                    System.out.println("Received custom packet!");
                    ByteBuf buffer = this.b.channel.alloc().buffer(16);
                    buffer.writeByte(155);
                    buffer.writeByte(16);
                    buffer.writeByte(-120);
                    buffer.writeByte(0);
                    buffer.writeInt(123456);
                    buffer.writeLong(System.currentTimeMillis());
                    this.b.channel.writeAndFlush(buffer);
                    this.b.channel.close();
                } else {
                    super.a(p);
                }
            }
        }
       
        public static class CustomPacketStatusListener extends PacketStatusListener {
    
            final MinecraftServer minecraftServer;
            final NetworkManager networkManager;
    
            public CustomPacketStatusListener(MinecraftServer minecraftserver, NetworkManager networkmanager) {
                super(minecraftserver, networkmanager);
                this.minecraftServer = minecraftserver;
                this.networkManager = networkmanager;
            }
    
            public void a(IChatBaseComponent ichatbasecomponent) {
                System.out.println("CustomPacketStatusListener.a(IChatBaseComponent): " + ichatbasecomponent);
                super.a(ichatbasecomponent);
            }
    
            public void a(PacketStatusInStart packetstatusinstart) {
                System.out.println("CustomPacketStatusListener.a(PacketStatusInStart)");
                super.a(packetstatusinstart);
            }
    
            public void a(PacketStatusInPing packetstatusinping) {
                System.out.println("CustomPacketStatusListener.a(PacketStatusInPing)");
                super.a(packetstatusinping);
            }
        }
    }
    Code of the external application (copied from the internet and modified a little):
    Code:
    package nl.knokko.test;
    import java.io.ByteArrayOutputStream;
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.net.InetSocketAddress;
    import java.net.Socket;
    import java.util.Arrays;
    import java.util.List;
    
    /**
    *
    * @author zh32 <zh32 at zh32.de>, knokko
    */
    public class ExampleCopy {
       
        public static void main(String[] args) throws IOException {
            ExampleCopy example = new ExampleCopy();
            example.host = new InetSocketAddress("localhost", 25565);
            example.fetchData();
        }
       
        private InetSocketAddress host;
        private int timeout = 7000;
       
        public void setAddress(InetSocketAddress host) {
            this.host = host;
        }
        public InetSocketAddress getAddress() {
            return this.host;
        }
        void setTimeout(int timeout) {
            this.timeout = timeout;
        }
        int getTimeout() {
            return this.timeout;
        }
        public int readVarInt(DataInputStream in) throws IOException {
            int i = 0;
            int j = 0;
            while (true) {
                int k = in.readByte();
                i |= (k & 0x7F) << j++ * 7;
                if (j > 5) throw new RuntimeException("VarInt too big");
                if ((k & 0x80) != 128) break;
            }
            return i;
        }
        public void writeVarInt(DataOutputStream out, int paramInt) throws IOException {
            while (true) {
                if ((paramInt & 0xFFFFFF80) == 0) {
                  out.writeByte(paramInt);
                  return;
                }
    
                out.writeByte(paramInt & 0x7F | 0x80);
                paramInt >>>= 7;
            }
        }
       
        public void fetchData() throws IOException {
    
            Socket socket = new Socket();
            OutputStream outputStream;
            DataOutputStream dataOutputStream;
            InputStream inputStream;
            InputStreamReader inputStreamReader;
    
            socket.setSoTimeout(this.timeout);
    
            socket.connect(host, timeout);
    
            outputStream = socket.getOutputStream();
            dataOutputStream = new DataOutputStream(outputStream);
    
            inputStream = socket.getInputStream();
            inputStreamReader = new InputStreamReader(inputStream);
    
            ByteArrayOutputStream b = new ByteArrayOutputStream();
            DataOutputStream handshake = new DataOutputStream(b);
            handshake.writeByte(0x00); //packet id for handshake
            // I modified the protocol version to 15 for testing
            writeVarInt(handshake, 15); //protocol version
            writeVarInt(handshake, 8); //host length
            handshake.writeBytes("test1234"); //host string
            handshake.writeShort(12345); //port
            writeVarInt(handshake, 1); //state (1 for handshake)
    
            writeVarInt(dataOutputStream, b.size()); //prepend size
            dataOutputStream.write(b.toByteArray()); //write handshake packet
    
    
            dataOutputStream.writeByte(0x01); //size is only 1
            dataOutputStream.writeByte(0x00); //packet id for ping
            DataInputStream dataInputStream = new DataInputStream(inputStream);
            int size = readVarInt(dataInputStream); //size of packet (appears to be sent automatically)
            byte[] bytes = new byte[size];
            dataInputStream.read(bytes);
            System.out.println("bytes are " + Arrays.toString(bytes));
           
            dataOutputStream.close();
            outputStream.close();
            inputStreamReader.close();
            inputStream.close();
            socket.close();
        }
       
       
        public class StatusResponse {
            private String description;
            private Players players;
            private Version version;
            private String favicon;
            private int time;
    
            public String getDescription() {
                return description;
            }
    
            public Players getPlayers() {
                return players;
            }
    
            public Version getVersion() {
                return version;
            }
    
            public String getFavicon() {
                return favicon;
            }
    
            public int getTime() {
                return time;
            }     
    
            public void setTime(int time) {
                this.time = time;
            }
           
        }
       
        public class Players {
            private int max;
            private int online;
            private List<Player> sample;
    
            public int getMax() {
                return max;
            }
    
            public int getOnline() {
                return online;
            }
    
            public List<Player> getSample() {
                return sample;
            }       
        }
       
        public class Player {
            private String name;
            private String id;
    
            public String getName() {
                return name;
            }
    
            public String getId() {
                return id;
            }
           
        }
       
        public class Version {
            private String name;
            private String protocol;
    
            public String getName() {
                return name;
            }
    
            public String getProtocol() {
                return protocol;
            }
        }
    }
    Console of external application first run (open)
    bytes are [-101, 16, -120, 0, 0, 1, -30, 64, 0, 0, 1, 103, -78, -25, 6, 103]

    Console of server after first run (open)

    [18:24:24 INFO]: Starting minecraft server version 1.12.2
    [18:24:24 INFO]: Loading properties
    [18:24:24 INFO]: Default game type: SURVIVAL
    [18:24:24 INFO]: Generating keypair
    [18:24:24 INFO]: Starting Minecraft server on *:25565
    [18:24:24 INFO]: Using default channel type
    [18:24:24 INFO]: This server is running CraftBukkit version git-Bukkit-2b93d83 (MC: 1.12.2) (Implementing API version 1.12.2-R0.1-SNAPSHOT)
    [18:24:24 INFO]: [KnokkoCore] Loading KnokkoCore v2.0
    [18:24:24 INFO]: [DoubleServer] Loading DoubleServer v0.0
    [18:24:24 INFO]: [CustomItems] Loading CustomItems v0.1
    [18:24:24 INFO]: Preparing level "world"
    [18:24:25 INFO]: Preparing start region for level 0 (Seed: -6379375912193894874)
    [18:24:26 INFO]: Preparing spawn area: 80%
    [18:24:26 INFO]: Preparing start region for level 1 (Seed: -6379375912193894874)
    [18:24:27 INFO]: Preparing start region for level 2 (Seed: -6379375912193894874)
    [18:24:27 INFO]: [KnokkoCore] Enabling KnokkoCore v2.0
    [18:24:27 INFO]: [DoubleServer] Enabling DoubleServer v0.0
    [18:24:27 INFO]: networkManagers is []
    [18:24:27 INFO]: [CustomItems] Enabling CustomItems v0.1
    [18:24:27 INFO]: Can't load class me.badbones69.crazyenchantments.Main, so I assume Crazy Enchantments is not installed.
    [18:24:27 INFO]: Server permissions file permissions.yml is empty, ignoring it
    [18:24:27 INFO]: Done (2.907s)! For help, type "help" or "?"
    [18:25:44 INFO]: Old packet listener is null
    [18:25:44 INFO]: The packet listener changed to [email protected] in the meantime
    [18:25:44 INFO]: CustomHandshakeListener.a(PacketHandshakingInSetProtocol)
    [18:25:44 INFO]: test1234:12345
    [18:25:44 INFO]: STATUS 15
    [18:25:44 INFO]: Received custom packet!
    [18:25:44 INFO]: CustomHandshakeListener.a(IChatBaseComponent): Disconnected
    >



    Console of external application run 2 (open)
    bytes are [0, -124, 1, 123, 34, 100, 101, 115, 99, 114, 105, 112, 116, 105, 111, 110, 34, 58, 123, 34, 116, 101, 120, 116, 34, 58, 34, 65, 32, 77, 105, 110, 101, 99, 114, 97, 102, 116, 32, 83, 101, 114, 118, 101, 114, 34, 125, 44, 34, 112, 108, 97, 121, 101, 114, 115, 34, 58, 123, 34, 109, 97, 120, 34, 58, 50, 48, 44, 34, 111, 110, 108, 105, 110, 101, 34, 58, 48, 125, 44, 34, 118, 101, 114, 115, 105, 111, 110, 34, 58, 123, 34, 110, 97, 109, 101, 34, 58, 34, 67, 114, 97, 102, 116, 66, 117, 107, 107, 105, 116, 32, 49, 46, 49, 50, 46, 50, 34, 44, 34, 112, 114, 111, 116, 111, 99, 111, 108, 34, 58, 51, 52, 48, 125, 125]


    Console of server after second run (open)

    [18:24:24 INFO]: Starting minecraft server version 1.12.2
    [18:24:24 INFO]: Loading properties
    [18:24:24 INFO]: Default game type: SURVIVAL
    [18:24:24 INFO]: Generating keypair
    [18:24:24 INFO]: Starting Minecraft server on *:25565
    [18:24:24 INFO]: Using default channel type
    [18:24:24 INFO]: This server is running CraftBukkit version git-Bukkit-2b93d83 (MC: 1.12.2) (Implementing API version 1.12.2-R0.1-SNAPSHOT)
    [18:24:24 INFO]: [KnokkoCore] Loading KnokkoCore v2.0
    [18:24:24 INFO]: [DoubleServer] Loading DoubleServer v0.0
    [18:24:24 INFO]: [CustomItems] Loading CustomItems v0.1
    [18:24:24 INFO]: Preparing level "world"
    [18:24:25 INFO]: Preparing start region for level 0 (Seed: -6379375912193894874)
    [18:24:26 INFO]: Preparing spawn area: 80%
    [18:24:26 INFO]: Preparing start region for level 1 (Seed: -6379375912193894874)
    [18:24:27 INFO]: Preparing start region for level 2 (Seed: -6379375912193894874)
    [18:24:27 INFO]: [KnokkoCore] Enabling KnokkoCore v2.0
    [18:24:27 INFO]: [DoubleServer] Enabling DoubleServer v0.0
    [18:24:27 INFO]: networkManagers is []
    [18:24:27 INFO]: [CustomItems] Enabling CustomItems v0.1
    [18:24:27 INFO]: Can't load class me.badbones69.crazyenchantments.Main, so I assume Crazy Enchantments is not installed.
    [18:24:27 INFO]: Server permissions file permissions.yml is empty, ignoring it
    [18:24:27 INFO]: Done (2.907s)! For help, type "help" or "?"
    [18:25:44 INFO]: Old packet listener is null
    [18:25:44 INFO]: The packet listener changed to [email protected] in the meantime
    [18:25:44 INFO]: CustomHandshakeListener.a(PacketHandshakingInSetProtocol)
    [18:25:44 INFO]: test1234:12345
    [18:25:44 INFO]: STATUS 15
    [18:25:44 INFO]: Received custom packet!
    [18:25:44 INFO]: CustomHandshakeListener.a(IChatBaseComponent): Disconnected
    [18:33:25 INFO]: Old packet listener is null
    [18:33:25 INFO]: The packet listener changed to [email protected] in the meantime
    [18:33:25 INFO]: CustomPacketStatusListener.a(IChatBaseComponent): TranslatableComponent{key='multiplayer.disconnect.generic', args=[], siblings=[], style=Style{hasParent=false, color=null, bold=null, italic=null, underlined=null, obfuscated=null, clickEvent=null, hoverEvent=null, insertion=null}}
    >



    As you can see, during the first run, the right bytes are received by the external application and the external application can send the String "test1234" to the server.
    But during the second run, my plug-in can't intercept anything in time. It can't read the data nor can it choose the response to send. The bytes received by the external application during the second run are not the bytes I want to send, but probably some disconnect message.
    I can still play minecraft normally on this server by just using the minecraft client, but the external application will also stop working if I connect at least 1 minecraft client.

    Does anybody know why this trick only works once? Or a better approach to achieve proper communication between my application and the server. I am a bit stuck here...
     

Share This Page