713 lines
16 KiB
Dart
713 lines
16 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// 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("/")));
|
|
} 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 {};
|
|
}
|
|
}
|