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 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) { layer.writeBytes(data); }, onDone: () async { layer.resetPosition(); try { List dataHash = layer.readBytes(256); int sequenceID = layer.readLong(); print("Sequence ID in request: $sequenceID"); int numBytes = layer.readLong(); List 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); 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(); }, 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 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 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 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); Completer responseWait = Completer(); socket!.add(layer.bytes); socket!.listen((data) { reply.writeBytes(data); }, onDone: () async { // Validate response validity reply.resetPosition(); int sequence = reply.readLong(); int successReceipt = reply.readByte(); List serverHash = reply.readBytes(256); String srvHashStr = Hashing.bytes2Hash(serverHash); int numBytes = reply.readLong(); List 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: () { 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 close() async { await socket!.close(); connected = false; } } abstract class IPacket with NbtEncodable, JsonEncodable { String getChannelID(); // This function handles the packet Future handleServerPacket(); Future 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 js) {} @override String getChannelID() { return "StopServer"; } @override Future 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 toJson() { return {}; } @override Future handleClientPacket() { throw UnimplementedError(); } } class PacketResponse { static final nil = PacketResponse(replyDataTag: CompoundTag()); PacketResponse({required this.replyDataTag}); CompoundTag replyDataTag = CompoundTag(); } class PacketRegistry { Map _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 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 params) {} @override Map toJson() { return {}; // Operation is not supported at this time. } @override Future 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 params) { String cap = params['cap'] as String; payload = PacketRegistry().getPacket(cap); payload.fromJson(params['payload']); } @override String getChannelID() { return "C2SRequest"; } @override Future handleServerPacket() async { // This has no internal handling return payload.handleServerPacket(); } @override Map toJson() { return {"cap": payload.getChannelID(), "payload": payload.toJson()}; } @override Future 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 js) { clientVersion = js['version'] as String; } @override String getChannelID() { return "Ping"; } @override Future 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 toJson() { return {"version": clientVersion}; } @override Future handleClientPacket() { throw UnimplementedError(); } } mixin JsonEncodable { String encodeJson(); void decodeJson(String params); Map toJson(); void fromJson(Map js); } mixin NbtEncodable { Tag encodeTag(); void decodeTag(Tag tag); }