Begin to add a new ACL system
This commit is contained in:
parent
03fef9863f
commit
f7c71230f0
9 changed files with 293 additions and 44 deletions
|
@ -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");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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});
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,9 @@ class Settings {
|
|||
UUID remoteLoginToken = UUID.ZERO;
|
||||
|
||||
PacketClient? client;
|
||||
User? superuser;
|
||||
|
||||
User? loggedInUser;
|
||||
|
||||
StateMachine subsys = StateMachine();
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue