sk.mlobb.be.rcon.BERconClient Maven / Gradle / Ivy
The newest version!
package sk.mlobb.be.rcon;
import lombok.Getter;
import sk.mlobb.be.rcon.handler.ConnectionHandler;
import sk.mlobb.be.rcon.handler.ResponseHandler;
import sk.mlobb.be.rcon.model.BECommand;
import sk.mlobb.be.rcon.model.BELoginCredential;
import sk.mlobb.be.rcon.model.enums.LoggingLevel;
import sk.mlobb.be.rcon.model.command.BECommandType;
import sk.mlobb.be.rcon.model.configuration.BERconConfiguration;
import sk.mlobb.be.rcon.model.enums.BEConnectType;
import sk.mlobb.be.rcon.model.enums.BEDisconnectType;
import sk.mlobb.be.rcon.model.enums.BEMessageType;
import sk.mlobb.be.rcon.model.exception.BERconException;
import sk.mlobb.be.rcon.wrapper.DatagramChannelWrapper;
import sk.mlobb.be.rcon.wrapper.LogWrapper;
import java.io.IOException;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.DatagramChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.zip.CRC32;
/**
* The type BattlEye Rcon client.
*/
public class BERconClient {
private final LogWrapper log;
@Getter
private final AtomicBoolean connected;
@Getter
private Thread monitorThread;
@Getter
private Thread receiveThread;
private final List connectionHandlerList;
private final BERconConfiguration beRconConfiguration;
private final List responseHandlerList;
private final CRC32 crc32;
private DatagramChannelWrapper datagramChannelWrapper;
private DatagramChannel datagramChannel;
private Queue commandQueue;
private AtomicInteger sequenceNumber;
private AtomicLong lastReceivedTime;
private ByteBuffer receiveBuffer;
private AtomicLong lastSentTime;
private ByteBuffer sendBuffer;
private Boolean loggingEnabled = false;
/**
* Instantiates a new BattlEye Rcon client.
*
* @param beRconConfiguration the be rcon configuration
*/
public BERconClient(BERconConfiguration beRconConfiguration) {
this(beRconConfiguration, null);
}
/**
* Instantiates a new BattlEye Rcon client.
*
* @param beRconConfiguration the be rcon configuration
* @param log the log
*/
public BERconClient(BERconConfiguration beRconConfiguration, LogWrapper log) {
logIfEnabled("Initializing client", LoggingLevel.DEBUG);
this.beRconConfiguration = beRconConfiguration;
this.log = log;
if (log != null) {
logIfEnabled(String.format("Using logger: %s", log), LoggingLevel.DEBUG);
loggingEnabled = true;
}
crc32 = new CRC32();
connected = new AtomicBoolean(false);
connectionHandlerList = new ArrayList<>();
responseHandlerList = new ArrayList<>();
receiveThread = new Thread(getReceiveRunnable());
receiveThread.setName("rcdata-1");
monitorThread = new Thread(getMonitorRunnable());
monitorThread.setName("rcmonitor-1");
datagramChannelWrapper = new DatagramChannelWrapper();
}
/**
* Connect to BattlEye Rcon.
*
* @param beLoginCredential the be login credential
* @throws IOException the io exception
*/
public void connect(BELoginCredential beLoginCredential) throws IOException {
logIfEnabled("Connecting to BE Rcon ...", LoggingLevel.INFO);
datagramChannel = datagramChannelWrapper.open();
datagramChannel.connect(beLoginCredential.getHostAddress());
logIfEnabled("Datagram connected ...", LoggingLevel.DEBUG);
sendBuffer = ByteBuffer.allocate(datagramChannel.getOption(StandardSocketOptions.SO_SNDBUF));
sendBuffer.order(ByteOrder.LITTLE_ENDIAN);
receiveBuffer = ByteBuffer.allocate(datagramChannel.getOption(StandardSocketOptions.SO_RCVBUF));
receiveBuffer.order(ByteOrder.LITTLE_ENDIAN);
commandQueue = new ConcurrentLinkedDeque<>();
lastSentTime = new AtomicLong(System.currentTimeMillis());
lastReceivedTime = new AtomicLong(System.currentTimeMillis());
sequenceNumber = new AtomicInteger(-1);
try {
logIfEnabled(String.format("Starting thread: %s", receiveThread.getName()), LoggingLevel.DEBUG);
receiveThread.start();
logIfEnabled(String.format("Starting thread: %s", monitorThread.getName()), LoggingLevel.DEBUG);
monitorThread.start();
} catch (Exception e) {
throw new BERconException(String.format("Failed to receive/monitor BattlEye Rcon! Most probably is not " +
"running in: %s", beLoginCredential.getHostAddress()));
}
logIfEnabled("Perform login ...", LoggingLevel.INFO);
constructPacket(BEMessageType.LOGIN, -1, beLoginCredential.getHostPassword());
sendData();
}
/**
* Send command.
*
* @param commands the commands
*/
public void sendCommand(String... commands) {
final StringBuilder commandBuilder = new StringBuilder();
for (String command : commands) {
commandBuilder.append(' ');
commandBuilder.append(command);
}
addCommandToQueue(new BECommand(BEMessageType.COMMAND, commandBuilder.toString()));
}
/**
* Send command.
*
* @param commandType the command type
*/
public void sendCommand(BECommandType commandType) {
addCommandToQueue(new BECommand(BEMessageType.COMMAND, commandType.getCommand()));
}
/**
* Send command.
*
* @param commandType the command type
* @param commandArgs the command args
*/
public void sendCommand(BECommandType commandType, String... commandArgs) {
final StringBuilder commandBuilder = new StringBuilder(commandType.getCommand());
for (String arg : commandArgs) {
commandBuilder.append(' ');
commandBuilder.append(arg);
}
logIfEnabled(String.format("Sending command: %s", commandBuilder.toString()), LoggingLevel.DEBUG);
addCommandToQueue(new BECommand(BEMessageType.COMMAND, commandBuilder.toString()));
}
/**
* Disconnect.
*/
public void disconnect() {
disconnect(BEDisconnectType.MANUAL);
}
private void disconnect(BEDisconnectType disconnectType) {
try {
logIfEnabled("Disconnecting ...", LoggingLevel.INFO);
connected.set(false);
commandQueue = null;
datagramChannel.disconnect();
datagramChannelWrapper.close(datagramChannel);
receiveThread.interrupt();
monitorThread.interrupt();
receiveThread = null;
sendBuffer = null;
receiveBuffer = null;
fireConnectionDisconnectVent(disconnectType);
} catch (IOException e) {
logIfEnabled("Failed to disconnect !", LoggingLevel.WARNING, e);
}
}
/**
* Construct a packet following the BattlEye protocol
* http://www.battleye.com/downloads/BERConProtocol.txt
*/
private void constructPacket(BEMessageType messageType, int sequenceNumber, String command) {
logIfEnabled("Constructing packet ...", LoggingLevel.DEBUG);
sendBuffer.clear();
sendBuffer.put((byte) 'B');
sendBuffer.put((byte) 'E');
sendBuffer.position(6);
sendBuffer.put((byte) 0xFF);
sendBuffer.put(messageType.getType());
if (sequenceNumber >= 0) {
sendBuffer.put((byte) sequenceNumber);
}
if (command != null && !command.isEmpty()) {
sendBuffer.put(command.getBytes());
}
crc32.reset();
crc32.update(sendBuffer.array(), 6, sendBuffer.position() - 6);
sendBuffer.putInt(2, (int) crc32.getValue());
sendBuffer.flip();
}
private void sendData() {
logIfEnabled("Sending data ...", LoggingLevel.DEBUG);
if (datagramChannel.isConnected()) {
try {
datagramChannel.write(sendBuffer);
lastSentTime.set(System.currentTimeMillis());
Thread.sleep(beRconConfiguration.getConnectionDelay());
} catch (IOException e) {
throw new BERconException("Failed to send data !", e);
} catch (InterruptedException e) {
logIfEnabled("Interrupted !", LoggingLevel.WARNING, e);
Thread.currentThread().interrupt();
}
}
}
protected void sendNextCommand() {
logIfEnabled("Sending next command ...", LoggingLevel.DEBUG);
if (commandQueue != null && !commandQueue.isEmpty()) {
final BECommand beCommand = commandQueue.poll();
if (beCommand != null) {
constructPacket(beCommand.getMessageType(), nextSequenceNumber(), beCommand.getCommand());
sendData();
}
}
}
/**
* Receives and validates the incoming packet
* 'B'(0x42) | 'E'(0x45) | 4-byte CRC32 checksum of the subsequent bytes | 0xFF
*/
private boolean receiveData() throws IOException {
logIfEnabled("Receiving data ...", LoggingLevel.DEBUG);
receiveBuffer.clear();
int read = datagramChannel.read(receiveBuffer);
if (read < 7) {
return false;
}
receiveBuffer.flip();
if (receiveBuffer.get() != (byte) 'B' || receiveBuffer.get() != (byte) 'E') {
return false;
}
receiveBuffer.getInt();
return receiveBuffer.get() == (byte) 0xFF;
}
private int nextSequenceNumber() {
int tempSequenceNumber = sequenceNumber.get();
tempSequenceNumber = tempSequenceNumber == 255 ? 0 : tempSequenceNumber + 1;
sequenceNumber.set(tempSequenceNumber);
return sequenceNumber.get();
}
private void addCommandToQueue(BECommand command) {
logIfEnabled(String.format("Adding command to queue: %s", command.toString()), LoggingLevel.DEBUG);
if (!commandQueue.isEmpty()) {
commandQueue.add(command);
} else {
commandQueue.add(command);
sendNextCommand();
}
}
/**
* Add connection handler.
*
* @param connectionHandler the connection handler
*/
public void addConnectionHandler(ConnectionHandler connectionHandler) {
connectionHandlerList.add(connectionHandler);
}
/**
* Add response handler.
*
* @param responseHandler the response handler
*/
public void addResponseHandler(ResponseHandler responseHandler) {
responseHandlerList.add(responseHandler);
}
private void fireConnectionConnectedEvent(BEConnectType connectType) {
for (ConnectionHandler connectionHandler : connectionHandlerList) {
connectionHandler.onConnected(connectType);
}
}
private void fireConnectionDisconnectVent(BEDisconnectType disconnectType) {
for (ConnectionHandler connectionHandler : connectionHandlerList) {
connectionHandler.onDisconnected(disconnectType);
}
}
private void fireResponseEvent(String response) {
for (ResponseHandler responseHandler : responseHandlerList) {
responseHandler.onResponse(response);
}
}
private Runnable getReceiveRunnable() {
return () -> {
try {
while (datagramChannel.isConnected()) {
if (receiveData()) {
lastReceivedTime.set(System.currentTimeMillis());
BEMessageType messageType = BEMessageType.convertByteToPacketType(receiveBuffer.get());
switch (messageType) {
case LOGIN:
receiveLoginPacket();
break;
case COMMAND:
receiveCommandPacket();
break;
case SERVER:
receiveServerPacket();
break;
case UNKNOWN:
logIfEnabled("Received unknown packet!", LoggingLevel.WARNING);
break;
default:
logIfEnabled("Unknown packet", LoggingLevel.WARNING);
break;
}
}
}
} catch (IOException e) {
throw new BERconException("Receiving failed !", e);
}
};
}
protected void receiveServerPacket() {
byte serverSequenceNumber = receiveBuffer.get();
fireResponseEvent(new String(receiveBuffer.array(), receiveBuffer.position(), receiveBuffer.remaining()));
constructPacket(BEMessageType.SERVER, serverSequenceNumber, null);
sendData();
sendNextCommand();
}
protected void receiveCommandPacket() throws IOException {
receiveBuffer.get();
if (receiveBuffer.hasRemaining()) {
if (receiveBuffer.get() == 0x00) {
int totalPackets = receiveBuffer.get();
int packetIndex = receiveBuffer.get();
String[] messageArray = new String[totalPackets];
messageArray[packetIndex] = new String(receiveBuffer.array(), receiveBuffer.position(), receiveBuffer.remaining());
packetIndex++;
while (packetIndex < totalPackets) {
receiveData();
receiveBuffer.position(12);
messageArray[packetIndex] = new String(receiveBuffer.array(), receiveBuffer.position(), receiveBuffer.remaining());
packetIndex++;
}
StringBuilder completeMessage = new StringBuilder();
for (String message : messageArray) {
completeMessage.append(message);
}
fireResponseEvent(completeMessage.toString());
} else {
receiveBuffer.position(receiveBuffer.position() - 1);
fireResponseEvent(new String(receiveBuffer.array(), receiveBuffer.position(), receiveBuffer.remaining()));
}
sendNextCommand();
}
}
protected void receiveLoginPacket() {
try {
BEConnectType connectionResult = BEConnectType.convertByteToConnectType(receiveBuffer.array()[8]);
switch (connectionResult) {
case FAILURE:
fireConnectionConnectedEvent(BEConnectType.FAILURE);
disconnect(BEDisconnectType.CONNECTION_LOST);
break;
case SUCCESS:
fireConnectionConnectedEvent(BEConnectType.SUCCESS);
connected.set(true);
break;
default:
log.warn("Unknown login packet received!");
fireConnectionConnectedEvent(BEConnectType.UNKNOWN);
disconnect(BEDisconnectType.SOCKET_EXCEPTION);
break;
}
} catch (Exception e) {
throw new BERconException("Failed to login!", e);
}
}
private Runnable getMonitorRunnable() {
return () -> {
while (datagramChannel.isConnected()) {
try {
TimeUnit.SECONDS.sleep(1);
logIfEnabled(String.format("Last received packet time: %s", lastReceivedTime.get()),
LoggingLevel.DEBUG);
logIfEnabled(String.format("Last sent packet time: %s", lastSentTime.get()), LoggingLevel.DEBUG);
checkTimeout();
sentKeepAlive();
} catch (InterruptedException e) {
logIfEnabled("Interrupted !", LoggingLevel.WARNING, e);
Thread.currentThread().interrupt();
}
}
};
}
protected void checkTimeout() {
logIfEnabled("Checking timeout ...", LoggingLevel.DEBUG);
if (lastSentTime.get() - lastReceivedTime.get() > beRconConfiguration.getTimeoutTime()) {
disconnect(BEDisconnectType.CONNECTION_LOST);
}
}
protected void sentKeepAlive() {
logIfEnabled("Check if keep alive is needed ...", LoggingLevel.DEBUG);
if (System.currentTimeMillis() - lastSentTime.get() >= beRconConfiguration.getKeepAliveTime()) {
constructPacket(BEMessageType.COMMAND, nextSequenceNumber(), null);
sendData();
logIfEnabled("Sent empty packet for keep alive!", LoggingLevel.INFO);
}
}
private void logIfEnabled(String message, LoggingLevel level) {
logIfEnabled(message, level, null);
}
private void logIfEnabled(String message, LoggingLevel level, Throwable throwable) {
if (loggingEnabled) {
switch (level) {
case DEBUG:
debugLog(message);
break;
case INFO:
infoLog(message);
break;
case WARNING:
warnLog(message, throwable);
break;
default:
break;
}
}
}
private void warnLog(String message, Throwable throwable) {
if (throwable == null) {
log.warn(message);
return;
}
log.warn(message, throwable);
}
private void infoLog(String message) {
log.info(message);
}
private void debugLog(String message) {
log.debug(message);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy