Roll my own rcon implementation due to problems with other implementation

This commit is contained in:
zontreck 2024-06-04 13:30:12 -07:00
parent 74fe37d7d8
commit 15aae5c3d7
5 changed files with 96 additions and 412 deletions

95
lib/utils/rcon/Rcon.dart Normal file
View file

@ -0,0 +1,95 @@
import 'dart:convert';
import 'dart:io';
class RCONPacket {
int id;
int type;
String body;
RCONPacket(this.id, this.type, this.body);
List<int> toBytes() {
List<int> bodyBytes = utf8.encode(body);
int size = 10 + bodyBytes.length;
List<int> packet = [];
packet.addAll(_intToBytes(size));
packet.addAll(_intToBytes(id));
packet.addAll(_intToBytes(type));
packet.addAll(bodyBytes);
packet.addAll([0, 0]);
return packet;
}
static RCONPacket fromBytes(List<int> bytes) {
int size = _bytesToInt(bytes.sublist(0, 4));
int id = _bytesToInt(bytes.sublist(4, 8));
int type = _bytesToInt(bytes.sublist(8, 12));
String body =
utf8.decode(bytes.sublist(12, size + 2).sublist(0, size - 10));
return RCONPacket(id, type, body);
}
static List<int> _intToBytes(int value) {
return [
value & 0xFF,
(value >> 8) & 0xFF,
(value >> 16) & 0xFF,
(value >> 24) & 0xFF
];
}
static int _bytesToInt(List<int> bytes) {
return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
}
}
class RCONClient {
String host;
int port;
String password;
late Socket _socket;
late int _requestId;
RCONClient(this.host, this.port, this.password) {
_requestId = 0;
}
Future<void> connect() async {
_socket = await Socket.connect(host, port);
if (await _authenticate()) {
print('Authenticated successfully');
} else {
print('Authentication failed');
}
}
Future<bool> _authenticate() async {
RCONPacket packet = RCONPacket(_requestId++, 3, password);
_socket.add(packet.toBytes());
await _socket.flush();
RCONPacket response = await _readPacket();
return response.id == packet.id && response.type == 2;
}
Future<RCONPacket> _readPacket() async {
List<int> sizeBytes = await _socket.first;
int size = RCONPacket._bytesToInt(sizeBytes);
List<int> dataBytes = [];
while (dataBytes.length < size) {
dataBytes.addAll(await _socket.first);
}
return RCONPacket.fromBytes(sizeBytes + dataBytes);
}
Future<String> sendCommand(String command) async {
RCONPacket packet = RCONPacket(_requestId++, 2, command);
_socket.add(packet.toBytes());
await _socket.flush();
RCONPacket response = await _readPacket();
return response.body;
}
void close() {
_socket.close();
}
}

View file

@ -1,98 +0,0 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:libac_dart/utils/rcon/rcon_helpers.dart';
import 'package:libac_dart/utils/rcon/rcon_vars.dart';
/// Creates and stores a socket connected to the RCON server
/// with the given host (IP/FQDN) and port. Port defaults to
/// 25575 if no port is specified.
Future<void> createSocket(String host, {int port = 25575}) async {
// Creates the socket by connecting the socket to the specified
// host and port.
rconSck = await Socket.connect(host, port);
}
/// Closes the socket to the RCON server. Returns a bool that
/// specified whether the socket was successfully destroyed.
bool close() {
// Checks to ensure that the RCON socket exists.
if (rconSck == null) {
return false;
}
// Destroys the socket, which also closes the connection.
rconSck!.destroy();
return true;
}
/// Send a message with the given message ID and message payload.
/// Returns a boolean that specifies if the command was successful.
bool sendMsg(int msgID, String payload) {
// Ensures that the RCON socket exists.
if (rconSck == null) {
return false;
}
// Message length is the payload length + 10 to account
// for the headers and suffix.
int msgLen = 10 + payload.length;
// Creates the full RCON message.
Uint8List fullMsg = cM(msgLen, msgID, payload);
// Add the RCON message to the socket stream.
rconSck!.add(fullMsg);
print("mc_rcon: sent payload ($fullMsg) on socket");
return true;
}
/// Log in to the RCON server using the given socket and password.
/// Returns a boolean that specifies if the command was successful.
bool login(String password) {
// Sends an RCON message with request ID = 3 (authenticate)
// with the password as the payload.
return sendMsg(3, password);
}
/// Send the provided command to the RCON server using the
/// Returns a boolean that specifies if the command was successful.
bool sendCommand(String command) {
// Sends an RCON message with request ID = 2 (command)
// with the String command as the payload.
return sendMsg(2, command);
}
/// Starts listening on the socket for packets sent by the RCON
/// server. Returns a boolean that specifies if the socket has
/// started listening. Note: onData must accept a List<int> and
/// a String as the only parameters.
bool listen(Function onData) {
// Checks to ensure that the RCON socket exists.
if (rconSck == null) {
return false;
}
// Starts listening on the RCON socket.
// Calls the first handler if we receive data, calls onError
// if there is an error on the stream, calls onDone when the
// client or the server ends the connection.
rconSck!.listen(
(Uint8List data) {
pSR(data, onData);
},
onError: (error) {
print('mc_rcon: Error with the connection to the server: $error');
rconSck!.destroy();
},
onDone: () {
print('mc_rcon: The server has ended the connection.');
rconSck!.destroy();
},
cancelOnError: false,
);
return true;
}

View file

@ -1,301 +0,0 @@
import 'dart:typed_data';
import 'rcon_vars.dart';
/// Returns a Uint8Lit that contains only 4 integers starting
/// from original[start]. Start defaults to 0 if not set. If
/// 4 integers cannot be copied from the list (e.g. list length
/// is less than 4 or start is too large to copy 4 integers), a
/// list of [-1, -1, -1, -1] will be returned. Returns 0 in place
/// of any integer if that location is empty in the source list.
Uint8List _u8LCopy4(Uint8List original, {int start = 0}) {
// Ensure that the Uint8List is at least len 4 and that
// start is list.length-4 or less
if (original.length < 4 || start + 3 > original.length - 1) {
// If not, return a list that is all -1.
Uint8List errorList = Uint8List(4)
..[0] = -1
..[1] = -1
..[2] = -1
..[3] = -1;
return errorList;
}
// Creates a new length 4 Uint8List and sets each of the
// values to a value from the original list.
Uint8List copiedUint8s = Uint8List(4)
..[0] = original[start]
..[1] = original[start + 1]
..[2] = original[start + 2]
..[3] = original[start + 3];
return copiedUint8s;
}
/// Returns a Uint8Lit that contains only 4 integers starting
/// from original[start]. Start defaults to 0 if not set. If
/// 4 integers cannot be copied from the list (e.g. list length
/// is less than 4 or start is too large to copy 4 integers), a
/// list of [-1, -1, -1, -1] will be returned. Returns 0 in place
/// of any integer if that location is empty in the source list.
Uint8List u8LCopy4(Uint8List original, {int start = 0}) {
return _u8LCopy4(original, start: start);
}
/// Returns a little Endian int32 interpreted from 4 Uint8s
/// retrieved from data (Uint8List) starting at an index of
/// start (int). The Uint8list length must be at least 4 and
/// start must be data.length - 4 or smaller. Returns -1
/// if processing has failed.
int _processToInt32(Uint8List data, int start) {
// Checks to ensure that the Uint8List is longer than 4
// and that the starting index is valid to parse out a
// 32-bit integer.
if (data.length < 4 || start > (data.length - 4)) {
return -1;
}
// Copies the 4 uint8s we need to parse the int32.
Uint8List copiedUint8s = _u8LCopy4(data, start: start);
// I don't know what this does. But it gives me access
// to the list and the ability to parse out a 32-bit int.
var blob = ByteData.sublistView(copiedUint8s);
// Process out the 32-bit integer assuming little Endian
// (which is what the RCON protocol uses).
int processedInt32 = blob.getInt32(0, Endian.little);
return processedInt32;
}
/// Returns a little Endian int32 interpreted from 4 Uint8s
/// retrieved from data (Uint8List) starting at an index of
/// start (int). The Uint8list length must be at least 4 and
/// start must be data.length - 4 or smaller. Returns -1
/// if processing has failed.
int processToInt32(Uint8List data, int start) {
return _processToInt32(data, start);
}
/// Returns a List<int> that contains the 3 header integers
/// processed from the Uint8List of data.
List<int> _processHeaders(Uint8List data) {
// Processes out each of the 3 header integers.
int msgLength = _processToInt32(data, 0);
int respReqID = _processToInt32(data, 4);
int commandID = _processToInt32(data, 8);
// Creates a list made of the header integers.
List<int> msgHeader = [msgLength, respReqID, commandID];
return msgHeader;
}
/// Returns a List<int> that contains the 3 header integers
/// processed from the Uint8List of data.
// @visibleForTesting
// List<int> processHeaders(Uint8List data) {
// return _processHeaders(data);
// }
/// Returns a boolean that represents whether the response ID
/// represents a good packet. Good packet means response
/// ID == original request ID. Bad auth packet means response
/// ID == -1.
bool _processResponseID(int respID) {
if (respID == requestID) {
// If the response ID == original request ID,
// we received a good packet.
print("mc_rcon: Good packet received.");
return true;
} else if (respID == -1) {
// If the response ID == -1, we haven't authenticated
// properly, which can mean we sent the wrong password
// or we haven't authenticated yet.
print(
"mc_rcon: Bad authentication. Incorrect password or you haven't logged in yet.");
return false;
} else {
// Catch-all for all other response IDs. Should never trigger.
print("mc_rcon: Received unknown request ID.");
return false;
}
}
/// Returns a boolean that represents whether the response ID
/// represents a good packet. Good packet means response
/// ID == original request ID. Bad auth packet means response
/// ID == -1.
// @visibleForTesting
// bool processResponseID(int respID) {
// return _processResponseID(respID);
// }
/// Processes the server response (represented as a Uint8List)
/// and calls onData handler if we have received a good packet.
void _processServerResponse(Uint8List data, Function onData) {
// Parses out the message headers and payload.
List<int> rconHeaders = _processHeaders(data);
String payload = String.fromCharCodes(data, 12);
// Pulls out the messsage length to ensure the integrity
// of the message and the response ID to print.
int messageLen = rconHeaders[0];
int responseID = rconHeaders[1];
print("mc_rcon: Server response id: $responseID");
// Ensures that the data we recieved is the same
// as what the length of the message is supposed to be.
bool badMessage = (data.length == messageLen);
// Sends the headers and payload to the user handler function
// if the response is good (we receive our own request ID).
if (!badMessage && _processResponseID(responseID)) {
onData(rconHeaders, payload);
}
}
/// Processes the server response (represented as a Uint8List)
/// and calls onData handler if we have received a good packet.
void processServerResponse(Uint8List data, Function onData) {
return _processServerResponse(data, onData);
}
/// Processes the server response (represented as a Uint8List)
/// and calls onData handler if we have received a good packet.
void pSR(Uint8List data, Function onData) {
return _processServerResponse(data, onData);
}
/// Sets every int in dataList to the Uint32List using a ByteData buffer.
void _setUint32s(Uint32List int32List, List<int> dataList) {
// Views the buffer of the Uint32list.
ByteData bd = ByteData.view(int32List.buffer);
// Used to offset the bytes assigned to the ByteData.
// Otherwise we will overwrite the already written bytes.
int bdByteOffset = 0;
// Loops through every int data and assigns it as a little
// Endian Uint32 to the Uint32list's ByteData buffer.
for (int data in dataList) {
bd.setUint32(bdByteOffset, data, Endian.little);
bdByteOffset += 4;
}
}
/// Sets every int in dataList to the Uint32List using a ByteData buffer.
// @visibleForTesting
// void setUint32s(Uint32List int32List, List<int> dataList) {
// return _setUint32s(int32List, dataList);
// }
/// Returns a Uint8List that represents the header of
/// the RCON message. Assembles the header with the
/// specified length and message ID (i.e. type of message).
Uint8List _assembleHeader(int payloadLen, int msgID, [int? overrideReqID]) {
// Creates a new, length 3, Uint32List.
Uint32List headerAs32 = Uint32List(3);
// Sets the three Uint32s to the proper header integers.
_setUint32s(headerAs32, [
payloadLen,
overrideReqID != null ? overrideReqID : requestID,
msgID,
]);
// Transforms the header Uint32List into a Uint8List
// for transmission.
Uint8List header = headerAs32.buffer.asUint8List();
return header;
}
/// Returns a Uint8List that represents the header of
/// the RCON message. Assembles the header with the
/// specified length and message ID (i.e. type of message).
// @visibleForTesting
// Uint8List assembleHeader(int payloadLen, int msgID) {
// return _assembleHeader(payloadLen, msgID);
// }
/// Returns a Uint8List that represents the suffix of
/// the RCON message. The suffix is two NULLs in ASCII,
/// one to end the payload, and one to end the message.
Uint8List _assembleSuffix() {
// Creates a new, length 2, Uint8List.
Uint8List suffix = Uint8List(2);
// Views the buffer of the list so we can add to the list.
ByteData suffixBD = ByteData.view(suffix.buffer);
// Adds the two NULLs to the Uint8List.
suffixBD.setUint8(0, 0);
suffixBD.setUint8(1, 0);
return suffix;
}
/// Returns a Uint8List that represents the suffix of
/// the RCON message. The suffix is two NULLs in ASCII,
/// one to end the payload, and one to end the message.
// @visibleForTesting
// Uint8List assembleSuffix() {
// return _assembleSuffix();
// }
/// Assembles a list of Uint8Lists into one Uint8List.
Uint8List _assembleUint8Lists(List<Uint8List> msgParts) {
// Creates a new BytesBuilder to assemble the ints.
BytesBuilder bBuilder = BytesBuilder();
// Adds every list of Uint8s to the BB.
// This automatically compiles the list of ints.
for (Uint8List part in msgParts) {
bBuilder.add(part);
}
// Returns the BB's data as a Uint8list.
return bBuilder.toBytes();
}
/// Assembles a list of Uint8Lists into one Uint8List.
// @visibleForTesting
// Uint8List assembleUint8Lists(List<Uint8List> msgParts) {
// return _assembleUint8Lists(msgParts);
// }
/// Returns the whole RCON message as a Uint8List. Requires the
/// message length, message ID, and the payload.
Uint8List _createMessage(int msgLen, int msgID, String payload,
[int? overrideReqID]) {
// Example login data: (u__ refers to the type of unsigned integer)
// 0d00 0000 | dec0 ad0b | 0300 0000 | 3132 3300 00
// len u32 | pID u32 | cmdID u32 | payload/password u8
// Creates the 3 parts of the message using their respective
// constructors (2 are self-written, one built into Uint8List).
Uint8List header = _assembleHeader(msgLen, msgID, overrideReqID);
Uint8List msgAsIntList = Uint8List.fromList(payload.codeUnits);
Uint8List suffix = _assembleSuffix();
// Assembles the 3 parts into 1 Uint8List to return;
Uint8List fullMsg = _assembleUint8Lists([header, msgAsIntList, suffix]);
return fullMsg;
}
/// Returns the whole RCON message as a Uint8List. Requires the
/// message length, message ID, and the payload.
Uint8List createMessage(int msgLen, int msgID, String payload,
[int? overrideReqID]) {
return _createMessage(msgLen, msgID, payload, overrideReqID);
}
/// Returns the whole RCON message as a Uint8List. Requires the
/// message length, message ID, and the payload.
Uint8List cM(int msgLen, int msgID, String payload) {
return _createMessage(msgLen, msgID, payload);
}

View file

@ -1,12 +0,0 @@
import 'dart:io';
import 'dart:math';
/// The socket that is used to communicate with the RCON server.
/// Generated when createSocket() is run.
Socket? rconSck;
/// The randomly generated request ID that is sent with every
/// message to the RCON server. Used to ensure that the commands
/// sent to and received from the server are ours, and not another
/// user's.
int requestID = Random().nextInt(2147483647);

View file

@ -1,6 +1,6 @@
name: libac_dart
description: "Aria's Creations code library"
version: 1.0.26
version: 1.0.27
homepage: "https://zontreck.com"