import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:libac_dart/nbt/NbtUtils.dart'; import 'package:libac_dart/nbt/Stream.dart'; import 'package:libac_dart/nbt/Tag.dart'; import 'package:libac_dart/nbt/impl/CompoundTag.dart'; import 'package:libac_dart/nbt/impl/ListTag.dart'; import 'package:libac_dart/nbt/impl/StringTag.dart'; import 'package:libac_dart/packets/packets.dart'; import 'package:libac_dart/utils/Hashing.dart'; import 'package:libac_dart/utils/IOTools.dart'; import 'package:libac_dart/utils/uuid/NbtUUID.dart'; import 'package:libac_dart/utils/uuid/UUID.dart'; import 'package:servermanager/statemachine.dart'; import 'package:servermanager/structs/SessionData.dart'; import 'package:servermanager/structs/credentials.dart'; import 'package:servermanager/structs/discordHookHelper.dart'; import 'package:servermanager/structs/settings.dart'; class ClientPackets { static void register() { PacketRegistry reg = PacketRegistry(); reg.register(C2SLoginPacket(), () { return C2SLoginPacket(); }); reg.register(S2CLoginReply(), () { return S2CLoginReply(); }); reg.register(C2SRequestSettingsPacket(), () { return C2SRequestSettingsPacket(); }); reg.register(C2SUploadSettingsPacket(), () { return C2SUploadSettingsPacket(); }); reg.register(C2SRequestCreateBackup(), () { return C2SRequestCreateBackup(); }); reg.register(C2SRequestSnapshotList(), () { return C2SRequestSnapshotList(); }); reg.register(S2CSnapshotList(), () { return S2CSnapshotList(); }); reg.register(C2SRequestSnapshotDeletion(), () { return C2SRequestSnapshotDeletion(); }); reg.register(C2SRequestWorldRestore(), () { return C2SRequestWorldRestore(); }); } } class C2SLoginPacket implements IPacket { String username = ""; String password = ""; @override void decodeJson(String params) { fromJson(json.decode(params)); } @override void decodeTag(Tag tag) { CompoundTag ct = tag.asCompoundTag(); username = ct.get("username")!.asString(); password = ct.get("password")!.asString(); } @override NetworkDirection direction() { return NetworkDirection.ClientToServer; } @override String encodeJson() { return json.encode(toJson()); } @override Tag encodeTag() { CompoundTag tag = CompoundTag(); tag.put("username", StringTag.valueOf(username)); tag.put("password", StringTag.valueOf(Hashing.sha256Hash(password))); return tag; } @override void fromJson(Map js) { username = js['username'] as String; password = js['password'] as String; } @override String getChannelID() { return "Login"; } @override Future handleServerPacket() async { S2CResponse response = S2CResponse(); S2CLoginReply loginReply = S2CLoginReply(); // Attempt to log in. Settings settings = Settings(); if (settings.superuser.login(username, password)) { settings.remoteLoginToken = UUID.generate(4); loginReply.valid = true; loginReply.token = settings.remoteLoginToken; settings.superuser.sendDiscordActionLog("Login Success"); settings.loggedInUser = settings.superuser; } else { loginReply.valid = false; } // Properly handle the disabled account if (loginReply.valid && username == "_disabled") loginReply.valid = false; if (!loginReply.valid && settings.superuser.name != username) { // Check for a lower level user if (settings.inst!.admins.any((T) => T.name == username)) { User theUser = settings.inst!.admins.firstWhere((T) => T.name == username); if (theUser.login(username, password)) { settings.remoteLoginToken = UUID.generate(4); loginReply.valid = true; loginReply.token = settings.remoteLoginToken; theUser.sendDiscordActionLog("Login Success"); settings.loggedInUser = theUser; } else { loginReply.valid = false; theUser.sendDiscordActionLog("Login Failed"); } } } response.contents = loginReply.encodeTag().asCompoundTag(); return PacketResponse(replyDataTag: response.encodeTag().asCompoundTag()); } @override Map toJson() { return {"username": username, "password": Hashing.sha256Hash(password)}; } @override Future handleClientPacket() async {} } class S2CLoginReply implements IPacket { bool valid = false; UUID token = UUID.ZERO; @override void decodeJson(String params) { fromJson(json.decode(params)); } @override void decodeTag(Tag tag) { print("Decoding S2C LoginReply"); StringBuilder sb = StringBuilder(); Tag.writeStringifiedNamedTag(tag, sb, 0); print(sb); CompoundTag ct = tag as CompoundTag; if (ct.containsKey("valid")) valid = NbtUtils.readBoolean(ct, "valid"); if (ct.containsKey("token")) { token = NbtUtils.readUUID(ct, "token").toUUID(); } } @override NetworkDirection direction() { return NetworkDirection.ServerToClient; } @override String encodeJson() { return json.encode(toJson()); } @override Tag encodeTag() { CompoundTag tag = CompoundTag(); NbtUtils.writeBoolean(tag, "valid", valid); NbtUtils.writeUUID(tag, "token", NbtUUID.fromUUID(token)); return tag; } @override void fromJson(Map js) { valid = js['valid'] as bool; token = UUID.parse(js['token'] as String); } @override String getChannelID() { return "LoginReply"; } @override Future handleClientPacket() async { // Handle login finalization related stuff Settings settings = Settings(); settings.remoteLoginToken = token; } @override Future handleServerPacket() async { return PacketResponse.nil; // We only operate on the client } @override Map toJson() { return {"valid": valid, "token": token.toString()}; } } class C2SRequestSettingsPacket implements IPacket { CompoundTag serverSettings = CompoundTag(); @override void decodeJson(String params) { throw UnsupportedError("Json is unsupported by LibACNBT at this time"); } @override void decodeTag(Tag tag) { CompoundTag ct = tag as CompoundTag; serverSettings = ct.get("settings")!.asCompoundTag(); } @override NetworkDirection direction() { return NetworkDirection.ClientToServer; } @override String encodeJson() { return ""; } @override Tag encodeTag() { CompoundTag ct = CompoundTag(); ct.put("settings", Settings().serialize()); return ct; } @override void fromJson(Map js) {} @override String getChannelID() { return "C2SRequestSettings"; } @override Future handleClientPacket() async { Settings settings = Settings(); settings.deserialize(serverSettings); settings.server = false; } @override Future handleServerPacket() async { S2CResponse response = S2CResponse(); Settings settings = Settings(); serverSettings = settings.serialize(); response.contents = encodeTag().asCompoundTag(); return PacketResponse(replyDataTag: response.encodeTag().asCompoundTag()); } @override Map toJson() { return {}; } } class C2SUploadSettingsPacket implements IPacket { CompoundTag srvSettings = CompoundTag(); bool performRestart = false; @override void decodeJson(String params) {} @override void decodeTag(Tag tag) { CompoundTag ct = tag.asCompoundTag(); srvSettings = ct.get("settings")!.asCompoundTag(); performRestart = NbtUtils.readBoolean(ct, "restart"); } @override NetworkDirection direction() { return NetworkDirection.ClientToServer; } @override String encodeJson() { return ""; } @override Tag encodeTag() { CompoundTag tag = CompoundTag(); tag.put("settings", Settings().serialize()); NbtUtils.writeBoolean(tag, "restart", performRestart); return tag; } @override void fromJson(Map js) {} @override String getChannelID() { return "C2SUploadSettings"; } @override Future handleClientPacket() async { // No client response or handling needed } @override Future handleServerPacket() async { Settings settings = Settings(); CompoundTag currentSettings = settings.serialize(); try { settings.deserialize(srvSettings); settings.Write(); DiscordHookHelper.sendWebHook( settings.inst!.discord, DiscordHookProps.ONLINE_ALERT, "Server Wrapper Settings", "Server wrapper settings have been updated.\n\n${performRestart ? "A restart has been requested" : "A restart is not needed"}"); if (!performRestart) { return PacketResponse.nil; } // Send a webhook with all the mods listed String modListText = ""; for (var entry in settings.inst!.mods) { if (entry.enabled) modListText += "${entry.mod_name}\n"; } modListText = modListText.trim(); if (modListText.isEmpty) { DiscordHookHelper.sendWebHook( settings.inst!.discord, DiscordHookProps.INACTIVE, "Mod List", "The Server is currently vanilla"); } else { DiscordHookHelper.sendWebHook(settings.inst!.discord, DiscordHookProps.INACTIVE, "Mod List", modListText); } // Check if server is running, if not, stop immediately // If server is running, schedule restart for 1 minute and send a alert to all players, then perform stop or restart depending on if running in Pterodactyl Compatibility mode SessionData.shutdownMessage = "Server wrapper restart"; SessionData.timer.apply(60); SessionData.CURRENT_INTERVAL = WarnIntervals.NONE; if (settings.subsys.currentState == States.Inactive) { Timer.periodic(Duration(seconds: 10), (timer) { SessionData.shutdownPending = true; // Stop packet server PacketServer.socket!.close(); timer.cancel(); exit(0); }); // We give time to allow the server to shut down gracefully. } return PacketResponse.nil; } catch (E) { settings.deserialize(currentSettings); return PacketResponse.nil; } } @override Map toJson() { return {}; } } class C2SRequestCreateBackup implements IPacket { String fileName = ""; // File name of the backup @override void decodeJson(String params) { return; // Json not supported by this packet } @override void decodeTag(Tag tag) { if (tag is CompoundTag) { CompoundTag ct = tag.asCompoundTag(); fileName = ct.get("name")!.asString(); } } @override NetworkDirection direction() { return NetworkDirection.ClientToServer; } @override String encodeJson() { return "{}"; } @override Tag encodeTag() { CompoundTag ct = CompoundTag(); ct.put("name", StringTag.valueOf(fileName)); return ct; } @override void fromJson(Map js) {} @override String getChannelID() { return "C2SRequestCreateBackup"; } @override Future handleClientPacket() async { // This is not handled on the client at all } @override Future handleServerPacket() async { // Copy the world file to the new destination Settings settings = Settings(); File world = File(settings.getWorldGameDB()); if (world.existsSync()) { // We're good! // Begin copy operations String destinationFile = "$fileName.db"; PathHelper pth = PathHelper(pth: settings.getWorldSnapshotFolder()) .resolve(destinationFile); world.copy(pth.build()); settings.loggedInUser! .sendDiscordActionLog("Created a new backup named $fileName"); } return PacketResponse.nil; } @override Map toJson() { return {}; } } class C2SRequestSnapshotList implements IPacket { @override void decodeJson(String params) {} @override void decodeTag(Tag tag) {} @override NetworkDirection direction() { return NetworkDirection.ClientToServer; } @override String encodeJson() { return "{}"; } @override Tag encodeTag() { return CompoundTag(); } @override void fromJson(Map js) {} @override String getChannelID() { return "C2SRequestSnapshotList"; } @override Future handleClientPacket() async {} @override Future handleServerPacket() async { // Generate the list of all snapshot files Settings settings = Settings(); List snapshotFileNames = await settings.getWorldSnapshotFiles(); S2CResponse response = S2CResponse(); S2CSnapshotList snapshots = S2CSnapshotList(); List strippedFiles = []; for (String str in snapshotFileNames) { if (str.endsWith(".db")) { // add file name without db extension to the list String trimmedFileName = str.trim().substring(0, str.trim().length - 3); strippedFiles.add( trimmedFileName.substring(trimmedFileName.lastIndexOf("/") + 1)); } else { strippedFiles.add(str); } } snapshots.snapshotFileNames = strippedFiles; response.contents = snapshots.encodeTag().asCompoundTag(); return PacketResponse(replyDataTag: response.encodeTag().asCompoundTag()); } @override Map toJson() { return {}; } } class S2CSnapshotList implements IPacket { List snapshotFileNames = []; @override void decodeJson(String params) {} @override void decodeTag(Tag tag) { CompoundTag ct = tag.asCompoundTag(); ListTag lst = ct.get("items") as ListTag; for (int i = 0; i < lst.size(); i++) { snapshotFileNames.add(lst.get(i).asString()); } } @override NetworkDirection direction() { return NetworkDirection.ServerToClient; } @override String encodeJson() { return "[]"; } @override Tag encodeTag() { CompoundTag ct = CompoundTag(); ListTag lst = ListTag(); for (String str in snapshotFileNames) { lst.add(StringTag.valueOf(str)); } ct.put("items", lst); return ct; } @override void fromJson(Map js) {} @override String getChannelID() { return "S2CSnapshotList"; } @override Future handleClientPacket() async { // Oh hey, its us! // Put the list in the SessionData SessionData.IE_SNAPSHOTS = snapshotFileNames; } @override Future handleServerPacket() { throw UnimplementedError(); } @override Map toJson() { return {}; } } class C2SRequestSnapshotDeletion implements IPacket { String snapshotName = ""; @override void decodeJson(String params) {} @override void decodeTag(Tag tag) { CompoundTag ct = tag.asCompoundTag(); snapshotName = ct.get("name")!.asString(); } @override NetworkDirection direction() { return NetworkDirection.ClientToServer; } @override String encodeJson() { return "{}"; } @override Tag encodeTag() { CompoundTag ct = CompoundTag(); ct.put("name", StringTag.valueOf(snapshotName)); return ct; } @override void fromJson(Map js) {} @override String getChannelID() { return "C2SRequestSnapshotDeletion"; } @override Future handleClientPacket() async {} @override Future handleServerPacket() async { Settings settings = Settings(); String correctedName = "$snapshotName.db"; PathHelper ph = PathHelper(pth: settings.getWorldSnapshotFolder()) .resolve(correctedName); ph.deleteFile(); settings.loggedInUser!.sendDiscordActionLog( "Requested snapshot deletion of backup named: $snapshotName"); return PacketResponse.nil; } @override Map toJson() { return {}; } } class C2SRequestWorldRestore implements IPacket { String snapshot = ""; @override void decodeJson(String params) {} @override void decodeTag(Tag tag) { CompoundTag ct = tag.asCompoundTag(); snapshot = ct.get("name")!.asString(); } @override NetworkDirection direction() { return NetworkDirection.ClientToServer; } @override String encodeJson() { return "{}"; } @override Tag encodeTag() { CompoundTag ct = CompoundTag(); ct.put("name", StringTag.valueOf(snapshot)); return ct; } @override void fromJson(Map js) {} @override String getChannelID() { return "C2SRequestWorldRestore"; } @override Future handleClientPacket() async {} @override Future handleServerPacket() async { Settings settings = Settings(); SessionData.isWorldRestore = true; SessionData.snapshotToRestore = snapshot; SessionData.shutdownMessage = "A backup restore has been requested"; SessionData.timer.apply(30); SessionData.CURRENT_INTERVAL = WarnIntervals.NONE; settings.loggedInUser!.sendDiscordActionLog( "Requested world restore, and initiated a immediate restart. World restored: $snapshot"); return PacketResponse.nil; } @override Map toJson() { return {}; } }