ConanServerManager/lib/packets/ClientPackets.dart

726 lines
17 KiB
Dart

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<String, dynamic> js) {
username = js['username'] as String;
password = js['password'] as String;
}
@override
String getChannelID() {
return "Login";
}
@override
Future<PacketResponse> 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<String, dynamic> toJson() {
return {"username": username, "password": Hashing.sha256Hash(password)};
}
@override
Future<void> 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<String, dynamic> js) {
valid = js['valid'] as bool;
token = UUID.parse(js['token'] as String);
}
@override
String getChannelID() {
return "LoginReply";
}
@override
Future<void> handleClientPacket() async {
// Handle login finalization related stuff
Settings settings = Settings();
settings.remoteLoginToken = token;
}
@override
Future<PacketResponse> handleServerPacket() async {
return PacketResponse.nil; // We only operate on the client
}
@override
Map<String, dynamic> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SRequestSettings";
}
@override
Future<void> handleClientPacket() async {
Settings settings = Settings();
settings.deserialize(serverSettings);
settings.server = false;
}
@override
Future<PacketResponse> handleServerPacket() async {
S2CResponse response = S2CResponse();
Settings settings = Settings();
serverSettings = settings.serialize();
response.contents = encodeTag().asCompoundTag();
return PacketResponse(replyDataTag: response.encodeTag().asCompoundTag());
}
@override
Map<String, dynamic> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SUploadSettings";
}
@override
Future<void> handleClientPacket() async {
// No client response or handling needed
}
@override
Future<PacketResponse> 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;
} else {
// Send a webhook with all the mods listed
String modListText = "";
for (var entry in settings.inst!.mods) {
modListText += "${entry.mod_name}\n";
}
modListText = modListText.trim();
DiscordHookHelper.sendWebHook(settings.inst!.discord,
DiscordHookProps.INACTIVE, "Mod List Updated", 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<String, dynamic> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SRequestCreateBackup";
}
@override
Future<void> handleClientPacket() async {
// This is not handled on the client at all
}
@override
Future<PacketResponse> 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<String, dynamic> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SRequestSnapshotList";
}
@override
Future<void> handleClientPacket() async {}
@override
Future<PacketResponse> handleServerPacket() async {
// Generate the list of all snapshot files
Settings settings = Settings();
List<String> snapshotFileNames = await settings.getWorldSnapshotFiles();
S2CResponse response = S2CResponse();
S2CSnapshotList snapshots = S2CSnapshotList();
List<String> 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<String, dynamic> toJson() {
return {};
}
}
class S2CSnapshotList implements IPacket {
List<String> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "S2CSnapshotList";
}
@override
Future<void> handleClientPacket() async {
// Oh hey, its us!
// Put the list in the SessionData
SessionData.IE_SNAPSHOTS = snapshotFileNames;
}
@override
Future<PacketResponse> handleServerPacket() {
throw UnimplementedError();
}
@override
Map<String, dynamic> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SRequestSnapshotDeletion";
}
@override
Future<void> handleClientPacket() async {}
@override
Future<PacketResponse> 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<String, dynamic> 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<String, dynamic> js) {}
@override
String getChannelID() {
return "C2SRequestWorldRestore";
}
@override
Future<void> handleClientPacket() async {}
@override
Future<PacketResponse> 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<String, dynamic> toJson() {
return {};
}
}