Begin to add a new ACL system

This commit is contained in:
zontreck 2024-07-02 16:01:06 -07:00
parent 03fef9863f
commit f7c71230f0
9 changed files with 293 additions and 44 deletions

View file

@ -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");

View file

@ -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 {

View file

@ -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<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;
}

View file

@ -32,7 +32,7 @@ class CredentialsPrompt extends State<CredentialsPage> {
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<CredentialsPage> {
)));
}
}
class User {
String name;
String passwordHash;
String passwordSalt;
User({required this.name, required this.passwordHash, required this.passwordSalt});
}

View file

@ -77,6 +77,10 @@ class SnapshotsState extends State<SnapshotsPage> {
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<SnapshotListPage> {
),
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,
),
),
);
}

View file

@ -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<void> 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;
}
}

View file

@ -40,6 +40,9 @@ class Settings {
UUID remoteLoginToken = UUID.ZERO;
PacketClient? client;
User? superuser;
User? loggedInUser;
StateMachine subsys = StateMachine();

View file

@ -11,6 +11,8 @@ import 'package:servermanager/structs/serversettings.dart';
class SettingsEntry {
List<Mod> mods = [];
List<User> 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;
}
}

View file

@ -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: