first commit

This commit is contained in:
2023-12-30 11:34:35 +03:00
commit 09bfa2cd7f
6 changed files with 384 additions and 0 deletions

38
pom.xml Normal file
View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.andrewkydev</groupId>
<artifactId>ServerReloader</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<!-- Build an executable JAR -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
<mainClass>org.andrewkydev.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,9 @@
package org.andrewkydev.rcon.ex;
public class AuthenticationException extends Exception {
public AuthenticationException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,11 @@
package org.andrewkydev.rcon.ex;
import java.io.IOException;
public class MalformedPacketException extends IOException {
public MalformedPacketException(String message) {
super(message);
}
}