From f7c71230f0c616033090432be9a74493db0208f0 Mon Sep 17 00:00:00 2001 From: zontreck Date: Tue, 2 Jul 2024 16:01:06 -0700 Subject: [PATCH] Begin to add a new ACL system --- bin/server.dart | 4 + lib/main.dart | 21 +++-- lib/packets/ClientPackets.dart | 46 +++++++++-- lib/pages/credentials_prompt.dart | 10 +-- lib/pages/snapshots.dart | 104 +++++++++++++++++++------ lib/structs/credentials.dart | 122 ++++++++++++++++++++++++++++++ lib/structs/settings.dart | 3 + lib/structs/settingsEntry.dart | 23 ++++++ pubspec.yaml | 4 +- 9 files changed, 293 insertions(+), 44 deletions(-) diff --git a/bin/server.dart b/bin/server.dart index 71dbabb..1a109f2 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -5,6 +5,7 @@ import 'package:libac_dart/utils/IOTools.dart'; import 'package:servermanager/game.dart'; import 'package:servermanager/packets/ClientPackets.dart'; import 'package:servermanager/structs/SessionData.dart'; +import 'package:servermanager/structs/credentials.dart'; import 'package:servermanager/structs/settings.dart'; void main() async { @@ -35,6 +36,9 @@ void main() async { settings.Write(); print("Wrote settings.dat"); + settings.superuser = User.make(settings.serverLoginCreds.username, + settings.serverLoginCreds.password, UserLevel.Super_User); + print("Initializing SteamCMD"); await settings.initializeSteamCmd(); print("Initialized Steamcmd"); diff --git a/lib/main.dart b/lib/main.dart index 5257122..29edbf0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -80,12 +80,21 @@ class ServerPage extends StatelessWidget { loginResponse.handleClientPacket(); if (loginResponse.valid) { - S2CResponse settingsData = - await settings.client!.send(C2SRequestSettingsPacket(), true); - C2SRequestSettingsPacket settingsBack = - C2SRequestSettingsPacket(); - settingsBack.decodeTag(settingsData.contents); - settingsBack.handleClientPacket(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: + Text("Login Success - Downloading remote Settings"))); + while (true) { + try { + await Future.delayed(Duration(seconds: 5)); + S2CResponse settingsData = await settings.client! + .send(C2SRequestSettingsPacket(), true); + C2SRequestSettingsPacket settingsBack = + C2SRequestSettingsPacket(); + settingsBack.decodeTag(settingsData.contents); + settingsBack.handleClientPacket(); + break; + } catch (E) {} + } Navigator.pushNamed(context, "/home"); } else { diff --git a/lib/packets/ClientPackets.dart b/lib/packets/ClientPackets.dart index b31a71e..4b756e9 100644 --- a/lib/packets/ClientPackets.dart +++ b/lib/packets/ClientPackets.dart @@ -15,6 +15,7 @@ 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'; @@ -104,17 +105,40 @@ class C2SLoginPacket implements IPacket { // Attempt to log in. Settings settings = Settings(); - if (settings.serverLoginCreds.username == username && - Hashing.sha256Hash(settings.serverLoginCreds.password) == password) { + + 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 { - //print( - // "Login failure\n${settings.serverLoginCreds.username}:${username}\n${Hashing.sha256Hash(settings.serverLoginCreds.password)}:${password}"); 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()); } @@ -414,6 +438,9 @@ class C2SRequestCreateBackup implements IPacket { PathHelper pth = PathHelper(pth: settings.getWorldSnapshotFolder()) .resolve(destinationFile); world.copy(pth.build()); + + settings.loggedInUser! + .sendDiscordActionLog("Created a new backup named ${fileName}"); } return PacketResponse.nil; @@ -473,7 +500,8 @@ class C2SRequestSnapshotList implements IPacket { // add file name without db extension to the list String trimmedFileName = str.trim().substring(0, str.trim().length - 3); - strippedFiles.add(trimmedFileName); + strippedFiles + .add(trimmedFileName.substring(trimmedFileName.lastIndexOf("/"))); } else { strippedFiles.add(str); } @@ -606,6 +634,9 @@ class C2SRequestSnapshotDeletion implements IPacket { .resolve(correctedName); ph.deleteFile(); + settings.loggedInUser!.sendDiscordActionLog( + "Requested snapshot deletion of backup named: ${snapshotName}"); + return PacketResponse.nil; } @@ -658,12 +689,17 @@ class C2SRequestWorldRestore implements IPacket { @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; } diff --git a/lib/pages/credentials_prompt.dart b/lib/pages/credentials_prompt.dart index 7130073..8df9ee9 100644 --- a/lib/pages/credentials_prompt.dart +++ b/lib/pages/credentials_prompt.dart @@ -32,7 +32,7 @@ class CredentialsPrompt extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text("Conan Exiles Server Manager - Credentials"), + title: Text("Conan Exiles Server Manager - Credentials (Super User)"), backgroundColor: Color.fromARGB(255, 100, 0, 0), ), floatingActionButton: ElevatedButton( @@ -98,11 +98,3 @@ class CredentialsPrompt extends State { ))); } } - -class User { - String name; - String passwordHash; - String passwordSalt; - - User({required this.name, required this.passwordHash, required this.passwordSalt}); -} \ No newline at end of file diff --git a/lib/pages/snapshots.dart b/lib/pages/snapshots.dart index a59ef1f..2fbad18 100644 --- a/lib/pages/snapshots.dart +++ b/lib/pages/snapshots.dart @@ -77,6 +77,10 @@ class SnapshotsState extends State { if (SessionData.IE_SNAPSHOTS.isNotEmpty) { // Show the list! + await Navigator.pushNamed( + context, "/server/snapshots/restore"); + + SessionData.IE_SNAPSHOTS.clear(); } }, ) @@ -108,36 +112,92 @@ class SnapshotListState extends State { ), body: Padding( padding: EdgeInsets.all(8), - child: SingleChildScrollView( - child: ListView.builder(itemBuilder: (ctx, index) { - String filename = SessionData.IE_SNAPSHOTS[index]; - return ListTile( - title: Text(filename), - leading: Icon(Icons.photo), - subtitle: Text( - "No information is known about this snapshot at this time"), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - onPressed: () { + child: ListView.builder( + itemBuilder: (ctx, index) { + String filename = SessionData.IE_SNAPSHOTS[index]; + + return ListTile( + title: Text(filename), + subtitle: Text( + "No information is known about this snapshot at this time"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + onPressed: () async { // Send restore packet + var result = await showCupertinoDialog( + context: context, + builder: (builder) { + return CupertinoAlertDialog( + title: Text("DANGER"), + content: Text( + "This action cannot be reversed, are you sure you want to restore $filename?"), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: Text("YES")), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text("ABORT")) + ], + ); + }); + + if (result == null) return; + C2SRequestWorldRestore rwr = C2SRequestWorldRestore(); + rwr.snapshot = filename; }, - icon: Icon(Icons.restore)), - IconButton( - onPressed: () { + icon: Icon(Icons.restore), + ), + IconButton( + onPressed: () async { // Send deletion packet + var result = await showCupertinoDialog( + context: context, + builder: (builder) { + return AlertDialog( + icon: Icon(Icons.delete_forever), + title: Text("DANGER"), + content: Text( + "This action is not reversible. Are you sure?"), + actions: [ + ElevatedButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: Text("Yes")), + ElevatedButton( + onPressed: () { + Navigator.pop(context, null); + }, + child: Text("ABORT")) + ], + ); + }); + + if (result == null) return; + C2SRequestSnapshotDeletion rsd = C2SRequestSnapshotDeletion(); rsd.snapshotName = filename; - settings.client!.send(rsd, true); }, - icon: Icon(Icons.delete)), - ], - ), - ); - })), + icon: Icon(Icons.delete), + ), + ], + ), + onTap: () { + // Handle tap on the list tile if needed + }, + ); + }, + itemCount: SessionData.IE_SNAPSHOTS.length, + ), ), ); } diff --git a/lib/structs/credentials.dart b/lib/structs/credentials.dart index 3de9087..67b1d8c 100644 --- a/lib/structs/credentials.dart +++ b/lib/structs/credentials.dart @@ -1,5 +1,9 @@ import 'package:libac_dart/nbt/impl/CompoundTag.dart'; +import 'package:libac_dart/nbt/impl/IntTag.dart'; import 'package:libac_dart/nbt/impl/StringTag.dart'; +import 'package:libac_dart/utils/Hashing.dart'; +import 'package:servermanager/structs/discordHookHelper.dart'; +import 'package:servermanager/structs/settings.dart'; class Credentials { String username; @@ -28,3 +32,121 @@ class Credentials { static const TAG_USERNAME = "username"; static const TAG_PASSWORD = "password"; } + +class User { + String name; + String passwordHash; + String passwordSalt; + UserLevel permissions; + final String userHash; + + bool login(String username, String passwordHash) { + if (userHash != generateValidityCheck()) + return false; // User will be thrown away next time the data is reloaded + + if (name == username) { + if (Hashing.sha256Hash("${passwordSalt}:${passwordHash}") == + this.passwordHash) { + return true; + } else + return false; + } else + return false; + } + + Future sendDiscordActionLog(String actionMessage) async { + Settings settings = Settings(); + + DiscordHookHelper.sendWebHook( + settings.inst!.discord, + DiscordHookProps.ALERT, + "User Action Alert", + "${this}: ${actionMessage}"); + } + + User( + {required this.name, + required this.passwordHash, + required this.passwordSalt, + required this.permissions, + required this.userHash}); + + CompoundTag serialize() { + CompoundTag ct = CompoundTag(); + ct.put(TAG_NAME, StringTag.valueOf(name)); + ct.put(TAG_HASH, StringTag.valueOf(passwordHash)); + ct.put(TAG_SALT, StringTag.valueOf(passwordSalt)); + ct.put(TAG_PERMS, IntTag.valueOf(permissions.ord())); + ct.put(TAG_SEC_CODE, StringTag.valueOf(userHash)); + + return ct; + } + + static User deserialize(CompoundTag ct) { + return User( + name: ct.get(TAG_NAME)!.asString(), + passwordHash: ct.get(TAG_HASH)!.asString(), + passwordSalt: ct.get(TAG_SALT)!.asString(), + permissions: UserLevel.of(ct.get(TAG_PERMS)!.asInt()), + userHash: ct.get(TAG_SEC_CODE)!.asString()); + } + + factory User.make(String name, String password, UserLevel level) { + String salt = Hashing.sha256Hash( + "${Hashing.md5Hash("${Hashing.sha256Hash("${DateTime.now().millisecondsSinceEpoch}")}")}"); + String hash = Hashing.sha256Hash("${salt}:${Hashing.sha256Hash(password)}"); + String validityCode = generateValidityCode(name, hash, salt, level); + + return User( + name: name, + passwordHash: hash, + passwordSalt: salt, + permissions: level, + userHash: validityCode); + } + + String generateValidityCheck() { + return Hashing.sha256Hash( + "${name}:${passwordHash}:${passwordSalt}:${permissions.ord()}}"); + } + + static String generateValidityCode(String name, String passwordHash, + String passwordSalt, UserLevel permissions) { + return Hashing.sha256Hash( + "${name}:${passwordHash}:${passwordSalt}:${permissions.ord()}}"); + } + + static const TAG_NAME = "name"; + static const TAG_HASH = "hash"; + static const TAG_SALT = "salt"; + static const TAG_PERMS = "perms"; + static const TAG_SEC_CODE = "validity"; + + @override + String toString() { + return "${permissions.name}: ${name}"; + } +} + +enum UserLevel { + Super_User(0), + Administrator(1), + Operator(2), + None(99); + + final int _id; + const UserLevel(this._id); + int ord() { + return _id; + } + + static UserLevel of(int ord) { + for (var lvl in values) { + if (lvl.ord() == ord) { + return lvl; + } + } + + return UserLevel.None; + } +} diff --git a/lib/structs/settings.dart b/lib/structs/settings.dart index df249a4..5117fb7 100644 --- a/lib/structs/settings.dart +++ b/lib/structs/settings.dart @@ -40,6 +40,9 @@ class Settings { UUID remoteLoginToken = UUID.ZERO; PacketClient? client; + User? superuser; + + User? loggedInUser; StateMachine subsys = StateMachine(); diff --git a/lib/structs/settingsEntry.dart b/lib/structs/settingsEntry.dart index 523b797..ab80525 100644 --- a/lib/structs/settingsEntry.dart +++ b/lib/structs/settingsEntry.dart @@ -11,6 +11,8 @@ import 'package:servermanager/structs/serversettings.dart'; class SettingsEntry { List mods = []; + List admins = []; + DiscordHookProps discord = DiscordHookProps(url: "", serverName: "", enabled: false); Credentials? steam_creds; @@ -41,6 +43,20 @@ class SettingsEntry { tag.get(DiscordHookProps.TAG_NAME)!.asCompoundTag()); } + if (tag.containsKey("admins")) { + ListTag adminUsers = tag.get("admins")! as ListTag; + for (int i = 0; i < adminUsers.size(); i++) { + CompoundTag entry = adminUsers.get(i).asCompoundTag(); + User loadedUser = User.deserialize(entry); + if (loadedUser.userHash == loadedUser.generateValidityCheck()) { + st.admins.add(loadedUser); + } else { + print( + "/!\\ FATAL /!\\\n\n${loadedUser} failed to pass the validity check and has been tampered with"); + } + } + } + st.mods.clear(); ListTag lMods = tag.get("mods") as ListTag; for (Tag tag in lMods.value) { @@ -65,6 +81,13 @@ class SettingsEntry { tag.put(DiscordHookProps.TAG_NAME, discord.serialize()); + ListTag adminUsers = ListTag(); + for (User usr in admins) { + adminUsers.add(usr.serialize()); + } + + tag.put("admins", adminUsers); + return tag; } } diff --git a/pubspec.yaml b/pubspec.yaml index 171f36a..3ff092d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.1.0+32 +version: 1.1.0+33 environment: sdk: '>=3.1.4 <4.0.0' @@ -40,7 +40,7 @@ dependencies: crypto: libac_dart: hosted: https://git.zontreck.com/api/packages/AriasCreations/pub/ - version: 1.0.33 + version: 1.0.34 dev_dependencies: flutter_test: