LibAC-dart/lib/packets/packets.dart
2024-08-29 15:13:59 -07:00

565 lines
14 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:libac_dart/utils/Hashing.dart';
import '../nbt/NbtIo.dart';
import '../nbt/Stream.dart';
import '../nbt/Tag.dart';
import '../nbt/impl/CompoundTag.dart';
import '../nbt/impl/StringTag.dart';
class PacketServer {
static ServerSocket? socket;
static bool shouldRestart = true;
static Future<void> start(int port) async {
socket = await ServerSocket.bind(InternetAddress.anyIPv4, port);
print("Server now listening on port ${port}");
await for (var sock in socket!) {
S2CResponse response = S2CResponse();
print(
"New connection from ${sock.remoteAddress.address}:${sock.remotePort}");
ByteLayer layer = ByteLayer();
try {
sock.listen((data) async {
layer.writeBytes(data);
var oldPos = layer.currentPosition;
layer.resetPosition();
int pktTotalExpected = layer.readLong();
if (pktTotalExpected + 8 <= layer.length) {
// Allow Processing
} else {
layer.restorePosition(oldPos);
return;
}
layer.resetPosition();
layer.readLong(); // This is unused outside of the above sanity check.
try {
List<int> dataHash = layer.readBytes(256);
int sequenceID = layer.readLong();
print("Sequence ID in request: $sequenceID");
int numBytes = layer.readLong();
List<int> remainingBytes = layer.readBytes(numBytes);
String sha256OriginalHash = Hashing.bytes2Hash(dataHash);
String sha256Hash =
Hashing.bytes2Hash(Hashing.sha1Sum(remainingBytes));
if (sha256OriginalHash == sha256Hash) {
CompoundTag tag = await NbtIo.readFromStream(
Uint8List.fromList(remainingBytes));
StringBuilder builder = StringBuilder();
Tag.writeStringifiedNamedTag(tag, builder, 0);
print("Request from client: \n${builder}");
C2SRequestPacket request = C2SRequestPacket();
request.decodeTag(tag);
PacketResponse reply = await request.handleServerPacket();
// Server uses NBT to communicate
builder = StringBuilder();
Tag.writeStringifiedNamedTag(reply.replyDataTag, builder, 0);
print("Response to client: \n${builder}");
Uint8List nbtData = await NbtIo.writeToStream(reply.replyDataTag);
layer.clear();
layer.writeLong(sequenceID);
layer.writeByte(0xFF); // Successful receipt
layer.writeBytes(Hashing.sha256Sum(nbtData));
layer.writeLong(nbtData.lengthInBytes);
layer.writeBytes(nbtData);
nbtData = layer.bytes;
// NOTE: Added a length indicator because SocketServer is apparently... really really dumb in its impl, and has no way to know when all data has been received, so no special event. We just have to check for it based on this initial value.
layer.clear();
layer.writeLong(nbtData.lengthInBytes);
layer.writeBytes(nbtData);
sock.add(layer.bytes);
} else {
// Return a failure packet
layer.clear();
layer.writeLong(sequenceID);
layer.writeByte(0x00);
sock.add(layer.bytes); // Failure code.
print(
"ERROR: The inbound hash did not match real hash: $sha256OriginalHash != $sha256Hash\n> REFUSING TO PROCESS PACKET. SENDING ERROR CODE TO CLIENT");
}
} catch (E, stack) {
response.contents
.put("error", StringTag.valueOf("Malformed request packet"));
print(
"Something went wrong. Malformed request? \n\n${E}\n\n${stack}\n\n\n\n");
} finally {
await sock.flush();
sock.close();
}
layer.clear();
}, onDone: () {
layer.clear();
}, onError: (E) {
print("ERROR: ${E}");
sock.close();
layer.clear();
});
} catch (E) {
sock.close();
}
}
}
}
class PacketClient {
Socket? socket;
bool connected = false;
String lastIP = "";
int port = 25306;
int packetSequence = 0;
PacketClient();
Future<void> startConnect(String IPAddress, int port) async {
try {
socket = await Socket.connect(IPAddress, port);
connected = true;
lastIP = IPAddress;
this.port = port;
} catch (E) {
connected = false;
socket = null;
}
}
/// Tries to send a packet to the connected server
///
/// On success, returns either, the decoded [S2CResponse], or on error a S2CResponse containing an error and a stacktrace as [StringTag]
Future<S2CResponse> send(IPacket packet, bool shouldReconnect) async {
if (!connected) {
return S2CResponse();
}
C2SRequestPacket request = C2SRequestPacket();
request.payload = packet;
request.cap = packet.getChannelID();
bool success = false;
ByteLayer layer = ByteLayer();
Uint8List nbtData =
await NbtIo.writeToStream(request.encodeTag().asCompoundTag());
List<int> nbtDataHash = Hashing.sha256Sum(nbtData);
ByteLayer reply = ByteLayer();
CompoundTag NBTTag = CompoundTag();
while (!success) {
layer.clear();
layer.writeBytes(nbtDataHash);
layer.writeLong(packetSequence);
layer.writeLong(nbtData.lengthInBytes);
layer.writeBytes(nbtData);
var tmpBytes = layer.bytes;
layer.clear();
layer.writeLong(tmpBytes.lengthInBytes);
layer.writeBytes(tmpBytes);
Completer responseWait = Completer();
socket!.add(layer.bytes);
socket!.listen((data) async {
reply.writeBytes(data);
var oldPos = reply.currentPosition;
reply.resetPosition();
int lenOfReply = reply.readLong();
if (lenOfReply + 8 <= reply.length) {
// We can now process the data
} else {
reply.restorePosition(oldPos);
return;
}
// Validate response validity
reply.resetPosition();
reply.readLong(); // This is unused outside of the sanity check above.
int sequence = reply.readLong();
int successReceipt = reply.readByte();
List<int> serverHash = reply.readBytes(256);
String srvHashStr = Hashing.bytes2Hash(serverHash);
int numBytes = reply.readLong();
List<int> pktBytes = reply.readBytes(numBytes);
String pktHash = Hashing.bytes2Hash(Hashing.sha256Sum(pktBytes));
if (successReceipt == 0xFF &&
packetSequence == sequence &&
srvHashStr == pktHash) success = true;
if (success) {
NBTTag = await NbtIo.readFromStream(Uint8List.fromList(pktBytes));
}
responseWait.complete();
}, onError: (err) {
if (!responseWait.isCompleted) responseWait.complete();
});
await responseWait.future;
packetSequence++;
if (!success) await Future.delayed(Duration(seconds: 5));
}
CompoundTag ct = CompoundTag();
StringBuilder builder = StringBuilder();
Tag.writeStringifiedNamedTag(NBTTag, builder, 0);
print("Response from server: \n${builder}");
ct.put("result", NBTTag);
await close();
if (shouldReconnect) await startConnect(lastIP, port);
S2CResponse replyPkt = S2CResponse();
try {
replyPkt.decodeTag(ct.get("result")!.asCompoundTag());
} catch (E, stack) {
replyPkt.contents = CompoundTag(); // This is essentially a null response
replyPkt.contents.put("error", StringTag.valueOf(E.toString()));
replyPkt.contents.put("stacktrace", StringTag.valueOf(stack.toString()));
}
return replyPkt;
}
Future<void> close() async {
await socket!.close();
connected = false;
}
}
abstract class IPacket with NbtEncodable, JsonEncodable {
String getChannelID();
// This function handles the packet
Future<PacketResponse> handleServerPacket();
Future<void> handleClientPacket();
NetworkDirection direction();
}
class StopServerPacket extends IPacket {
@override
void decodeJson(String params) {}
@override
void decodeTag(Tag tag) {}
@override
NetworkDirection direction() {
return NetworkDirection.ClientToServer;
}
@override
String encodeJson() {
return json.encode({});
}
@override
Tag encodeTag() {
return CompoundTag();
}
@override
void fromJson(Map<String, dynamic> js) {}
@override
String getChannelID() {
return "StopServer";
}
@override
Future<PacketResponse> handleServerPacket() async {
// We're now on the server. Handle the packet with a response to the client
PacketServer.shouldRestart = false;
S2CResponse response = S2CResponse();
return PacketResponse(replyDataTag: response.encodeTag().asCompoundTag());
}
@override
Map<String, dynamic> toJson() {
return {};
}
@override
Future<void> handleClientPacket() {
throw UnimplementedError();
}
}
class PacketResponse {
static final nil = PacketResponse(replyDataTag: CompoundTag());
PacketResponse({required this.replyDataTag});
CompoundTag replyDataTag = CompoundTag();
}
class PacketRegistry {
Map<String, IPacket Function()> _registry = {};
static PacketRegistry _inst = PacketRegistry._();
PacketRegistry._() {
registerDefaults();
}
factory PacketRegistry() {
return _inst;
}
int get count => _registry.length;
void register(IPacket packet, IPacket Function() packetResolver) {
_registry[packet.getChannelID()] = packetResolver;
}
IPacket getPacket(String channel) {
if (_registry.containsKey(channel)) {
IPacket Function() callback = _registry[channel]!;
return callback();
} else
throw Exception("No such channel has been registered");
}
void registerDefaults() {
register(S2CResponse(), () {
return S2CResponse();
});
register(C2SRequestPacket(), () {
return C2SRequestPacket();
});
register(StopServerPacket(), () {
return StopServerPacket();
});
register(C2SPing(), () {
return C2SPing();
});
}
}
enum NetworkDirection { ClientToServer, ServerToClient }
enum PacketOperation { Encode, Decode }
class S2CResponse implements IPacket {
CompoundTag contents = CompoundTag();
@override
NetworkDirection direction() {
return NetworkDirection.ServerToClient;
}
@override
String getChannelID() {
return "Response";
}
@override
Future<PacketResponse> handleServerPacket() async {
// We can't predict handling for this type, it is a data packet response with no pre-defined structure.
return PacketResponse.nil;
}
@override
void decodeJson(String encoded) {
fromJson(json.decode(encoded));
}
@override
void decodeTag(Tag encoded) {
CompoundTag ct = encoded as CompoundTag;
contents = ct.get("contents")!.asCompoundTag();
}
@override
String encodeJson() {
return json.encode(toJson());
}
@override
Tag encodeTag() {
CompoundTag tag = CompoundTag();
tag.put("contents", contents);
return tag;
}
@override
void fromJson(Map<String, dynamic> params) {}
@override
Map<String, dynamic> toJson() {
return {}; // Operation is not supported at this time.
}
@override
Future<void> handleClientPacket() async {
// We haven't got anything to process. This is structured data
}
}
class C2SRequestPacket implements IPacket {
String cap = ""; // Packet channel
late IPacket payload;
@override
void decodeJson(String encoded) {
fromJson(json.decode(encoded));
}
@override
void decodeTag(Tag encoded) {
CompoundTag tag = encoded.asCompoundTag();
String cap = tag.get("cap")!.asString();
payload = PacketRegistry().getPacket(cap);
payload.decodeTag(tag.get("payload")!.asCompoundTag());
}
@override
NetworkDirection direction() {
return NetworkDirection.ClientToServer;
}
@override
String encodeJson() {
return json.encode(toJson());
}
@override
Tag encodeTag() {
CompoundTag tag = CompoundTag();
tag.put("cap", StringTag.valueOf(payload.getChannelID()));
tag.put("payload", payload.encodeTag());
return tag;
}
@override
void fromJson(Map<String, dynamic> params) {
String cap = params['cap'] as String;
payload = PacketRegistry().getPacket(cap);
payload.fromJson(params['payload']);
}
@override
String getChannelID() {
return "C2SRequest";
}
@override
Future<PacketResponse> handleServerPacket() async {
// This has no internal handling
return payload.handleServerPacket();
}
@override
Map<String, dynamic> toJson() {
return {"cap": payload.getChannelID(), "payload": payload.toJson()};
}
@override
Future<void> handleClientPacket() {
throw UnimplementedError();
}
}
class C2SPing implements IPacket {
String clientVersion = "";
@override
void decodeJson(String params) {
fromJson(json.decode(params));
}
@override
void decodeTag(Tag tag) {
clientVersion = tag.asCompoundTag().get("version")!.asString();
}
@override
NetworkDirection direction() {
return NetworkDirection.ClientToServer;
}
@override
String encodeJson() {
return json.encode(toJson());
}
@override
Tag encodeTag() {
CompoundTag tag = CompoundTag();
tag.put("version", StringTag.valueOf(clientVersion));
return tag;
}
@override
void fromJson(Map<String, dynamic> js) {
clientVersion = js['version'] as String;
}
@override
String getChannelID() {
return "Ping";
}
@override
Future<PacketResponse> handleServerPacket() async {
CompoundTag tag = CompoundTag();
tag.put("pong", StringTag.valueOf(Platform.version));
S2CResponse response = S2CResponse();
response.contents = tag;
PacketResponse reply =
PacketResponse(replyDataTag: response.encodeTag().asCompoundTag());
return reply;
}
@override
Map<String, dynamic> toJson() {
return {"version": clientVersion};
}
@override
Future<void> handleClientPacket() {
throw UnimplementedError();
}
}
mixin JsonEncodable {
String encodeJson();
void decodeJson(String params);
Map<String, dynamic> toJson();
void fromJson(Map<String, dynamic> js);
}
mixin NbtEncodable {
Tag encodeTag();
void decodeTag(Tag tag);
}