commit 09bfa2cd7f64cdbb78ac14fcfeda4217f93fd125 Author: andrewkydev Date: Sat Dec 30 11:34:35 2023 +0300 first commit diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2ea23d5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + org.andrewkydev + ServerReloader + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + lib/ + org.andrewkydev.Main + + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/andrewkydev/Main.java b/src/main/java/org/andrewkydev/Main.java new file mode 100644 index 0000000..8f93581 --- /dev/null +++ b/src/main/java/org/andrewkydev/Main.java @@ -0,0 +1,58 @@ +package org.andrewkydev; + +import org.andrewkydev.rcon.Rcon; +import org.andrewkydev.rcon.ex.AuthenticationException; + +import java.io.IOException; +import java.nio.file.*; + +/** + * The class watches a directory for changes and sends a reload command to the server when a file in the directory is modified. + */ +public class Main { + /** + * The main method of the class. + * + * @param args the command line arguments + */ + public static void main(String[] args) { + // The host and port of the Minecraft server + String host = "127.0.0.1"; + int port = 25575; + // The password of the RCON connection + String password = "1YjdkNTJiN"; + // The command to send to the server + String command = "reload true"; + // The directory to watch for changes + String pathToWatch = "/home/andrew/projects/1/plugins/"; + + try { + // Create a new watch service + WatchService watchService = FileSystems.getDefault().newWatchService(); + + // Register the directory with the watch service + Path path = Paths.get(pathToWatch); + path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY); + + // Wait for changes + WatchKey key; + while ((key = watchService.take()) != null) { + // Process the events + for (WatchEvent event : key.pollEvents()) { + // Print the event details + System.out.println("Event kind: " + event.kind() + ". File affected: " + event.context() + "."); + if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) { + // Create a new Rcon instance and send the command + Rcon rcon = new Rcon(host, port, password.getBytes()); + String response = rcon.command(command); + System.out.println(response); + } + } + // Reset the key + key.reset(); + } + } catch (IOException | InterruptedException | AuthenticationException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/andrewkydev/rcon/Rcon.java b/src/main/java/org/andrewkydev/rcon/Rcon.java new file mode 100644 index 0000000..31a6231 --- /dev/null +++ b/src/main/java/org/andrewkydev/rcon/Rcon.java @@ -0,0 +1,124 @@ +package org.andrewkydev.rcon; + + +import org.andrewkydev.rcon.ex.AuthenticationException; + +import java.io.IOException; +import java.net.Socket; +import java.nio.charset.Charset; +import java.util.Random; + +public class Rcon { + + private final Object sync = new Object(); + private final Random rand = new Random(); + + private int requestId; + private Socket socket; + + private Charset charset; + + /** + * Create, connect and authenticate a new Rcon object + * + * @param host Rcon server address + * @param port Rcon server port + * @param password Rcon server password + * @throws IOException + * @throws AuthenticationException + */ + public Rcon(String host, int port, byte[] password) throws IOException, AuthenticationException { + // Default charset is utf8 + this.charset = Charset.forName("UTF-8"); + + // Connect to host + this.connect(host, port, password); + } + + /** + * Connect to a rcon server + * + * @param host Rcon server address + * @param port Rcon server port + * @param password Rcon server password + * @throws IOException + * @throws AuthenticationException + */ + public void connect(String host, int port, byte[] password) throws IOException, AuthenticationException { + if (host == null || host.trim().isEmpty()) { + throw new IllegalArgumentException("Host can't be null or empty"); + } + + if (port < 1 || port > 65535) { + throw new IllegalArgumentException("Port is out of range"); + } + + // Connect to the rcon server + synchronized (sync) { + // New random request id + this.requestId = rand.nextInt(); + + // We can't reuse a socket, so we need a new one + this.socket = new Socket(host, port); + } + + // Send the auth packet + RconPacket res = this.send(RconPacket.SERVERDATA_AUTH, password); + + // Auth failed + if (res.getRequestId() == -1) { + throw new AuthenticationException("Password rejected by server"); + } + } + + /** + * Disconnect from the current server + * + * @throws IOException + */ + public void disconnect() throws IOException { + synchronized (sync) { + this.socket.close(); + } + } + + /** + * Send a command to the server + * + * @param payload The command to send + * @return The payload of the response + * @throws IOException + */ + public String command(String payload) throws IOException { + if (payload == null || payload.trim().isEmpty()) { + throw new IllegalArgumentException("Payload can't be null or empty"); + } + + RconPacket response = this.send(RconPacket.SERVERDATA_EXECCOMMAND, payload.getBytes()); + + return new String(response.getPayload(), this.getCharset()); + } + + private RconPacket send(int type, byte[] payload) throws IOException { + synchronized (sync) { + return RconPacket.send(this, type, payload); + } + } + + public int getRequestId() { + return requestId; + } + + public Socket getSocket() { + return socket; + } + + public Charset getCharset() { + return charset; + } + + public void setCharset(Charset charset) { + this.charset = charset; + } + +} diff --git a/src/main/java/org/andrewkydev/rcon/RconPacket.java b/src/main/java/org/andrewkydev/rcon/RconPacket.java new file mode 100644 index 0000000..ef223c3 --- /dev/null +++ b/src/main/java/org/andrewkydev/rcon/RconPacket.java @@ -0,0 +1,144 @@ +package org.andrewkydev.rcon; + + +import org.andrewkydev.rcon.ex.MalformedPacketException; + +import java.io.*; +import java.net.SocketException; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class RconPacket { + + public static final int SERVERDATA_EXECCOMMAND = 2; + public static final int SERVERDATA_AUTH = 3; + + private int requestId; + private int type; + private byte[] payload; + + private RconPacket(int requestId, int type, byte[] payload) { + this.requestId = requestId; + this.type = type; + this.payload = payload; + } + + public int getRequestId() { + return requestId; + } + + public int getType() { + return type; + } + + public byte[] getPayload() { + return payload; + } + + /** + * Send a Rcon packet and fetch the response + * + * @param rcon Rcon instance + * @param type The packet type + * @param payload The payload (password, command, etc.) + * @return A RconPacket object containing the response + * @throws IOException + * @throws MalformedPacketException + */ + protected static RconPacket send(Rcon rcon, int type, byte[] payload) throws IOException { + try { + RconPacket.write(rcon.getSocket().getOutputStream(), rcon.getRequestId(), type, payload); + } catch (SocketException se) { + // Close the socket if something happens + rcon.getSocket().close(); + + // Rethrow the exception + throw se; + } + + return RconPacket.read(rcon.getSocket().getInputStream()); + } + + /** + * Write a rcon packet on an outputstream + * + * @param out The OutputStream to write on + * @param requestId The request id + * @param type The packet type + * @param payload The payload + * @throws IOException + */ + private static void write(OutputStream out, int requestId, int type, byte[] payload) throws IOException { + int bodyLength = RconPacket.getBodyLength(payload.length); + int packetLength = RconPacket.getPacketLength(bodyLength); + + ByteBuffer buffer = ByteBuffer.allocate(packetLength); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + buffer.putInt(bodyLength); + buffer.putInt(requestId); + buffer.putInt(type); + buffer.put(payload); + + // Null bytes terminators + buffer.put((byte) 0); + buffer.put((byte) 0); + + // Woosh! + out.write(buffer.array()); + out.flush(); + } + + /** + * Read an incoming rcon packet + * + * @param in The InputStream to read on + * @return The read RconPacket + * @throws IOException + * @throws MalformedPacketException + */ + private static RconPacket read(InputStream in) throws IOException { + // Header is 3 4-bytes ints + byte[] header = new byte[4 * 3]; + + // Read the 3 ints + in.read(header); + + try { + // Use a bytebuffer in little endian to read the first 3 ints + ByteBuffer buffer = ByteBuffer.wrap(header); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + int length = buffer.getInt(); + int requestId = buffer.getInt(); + int type = buffer.getInt(); + + // Payload size can be computed now that we have its length + byte[] payload = new byte[length - 4 - 4 - 2]; + + DataInputStream dis = new DataInputStream(in); + + // Read the full payload + dis.readFully(payload); + + // Read the null bytes + dis.read(new byte[2]); + + return new RconPacket(requestId, type, payload); + } catch (BufferUnderflowException | EOFException e) { + throw new MalformedPacketException("Cannot read the whole packet"); + } + } + + private static int getPacketLength(int bodyLength) { + // 4 bytes for length + x bytes for body length + return 4 + bodyLength; + } + + private static int getBodyLength(int payloadLength) { + // 4 bytes for requestId, 4 bytes for type, x bytes for payload, 2 bytes for two null bytes + return 4 + 4 + payloadLength + 2; + } + +} \ No newline at end of file diff --git a/src/main/java/org/andrewkydev/rcon/ex/AuthenticationException.java b/src/main/java/org/andrewkydev/rcon/ex/AuthenticationException.java new file mode 100644 index 0000000..28fe2c4 --- /dev/null +++ b/src/main/java/org/andrewkydev/rcon/ex/AuthenticationException.java @@ -0,0 +1,9 @@ +package org.andrewkydev.rcon.ex; + +public class AuthenticationException extends Exception { + + public AuthenticationException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/src/main/java/org/andrewkydev/rcon/ex/MalformedPacketException.java b/src/main/java/org/andrewkydev/rcon/ex/MalformedPacketException.java new file mode 100644 index 0000000..ffdb1fe --- /dev/null +++ b/src/main/java/org/andrewkydev/rcon/ex/MalformedPacketException.java @@ -0,0 +1,11 @@ +package org.andrewkydev.rcon.ex; + +import java.io.IOException; + +public class MalformedPacketException extends IOException { + + public MalformedPacketException(String message) { + super(message); + } + +}