Added support for on-the-fly textview contents updates Added iterative view removal to ConsoleController Added dumy layouts to Common Added support for account removal Fixed command management (now supports leading, padding and trailing spaces) Various smaller changes
571 lines
33 KiB
C#
571 lines
33 KiB
C#
using Tofvesson.Common;
|
|
using Tofvesson.Common.Cryptography.KeyExchange;
|
|
using Server.Properties;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using Tofvesson.Net;
|
|
using Tofvesson.Crypto;
|
|
|
|
namespace Server
|
|
{
|
|
class Program
|
|
{
|
|
// Message-of-the-day (for all days)
|
|
private const string CONSOLE_MOTD = @"Tofvesson Enterprises Banking Server
|
|
By Gabriel Tofvesson
|
|
|
|
Use command 'help' to get a list of available commands";
|
|
|
|
// Specific error reference localization prefix
|
|
private const string VERBOSE_RESPONSE = "@string/REMOTE_";
|
|
public static int verbosity = 2;
|
|
const decimal ratePerDay = 0.5M; // Savings account has a growth rate of 150% per day
|
|
|
|
// Initialize the database
|
|
public static readonly Database db = new Database("BankDB", "Resources");
|
|
public static void Main(string[] args)
|
|
{
|
|
// Create a client session manager and allow sessions to remain valid for up to 5 minutes of inactivity (300 seconds)
|
|
SessionManager manager = new SessionManager(300 * TimeSpan.TicksPerSecond, 20);
|
|
|
|
SetConsoleCtrlHandler(i => {
|
|
db.Flush(); // Ensures that the database is flushed before the program exits
|
|
return false;
|
|
}, true);
|
|
|
|
// Create a secure random provider and start getting RSA stuff
|
|
CryptoRandomProvider random = new CryptoRandomProvider();
|
|
Task<RSA> t = new Task<RSA>(() =>
|
|
{
|
|
RSA rsa = new RSA(Resources.e_0x100, Resources.n_0x100, Resources.d_0x100);
|
|
if (rsa == null)
|
|
{
|
|
Output.Fatal("No RSA keys found! Server identity will not be verifiable!");
|
|
Output.Info("Generating session-specific RSA-keys...");
|
|
rsa = new RSA(128, 8, 7, 5);
|
|
rsa.Save("0x100");
|
|
Output.Info("Done!");
|
|
}
|
|
return rsa;
|
|
});
|
|
t.Start();
|
|
|
|
// Local methods to simplify common operations
|
|
bool ParseDataPair(string cmd, out string user, out string pass)
|
|
{
|
|
int idx = cmd.IndexOf(':');
|
|
user = "";
|
|
pass = "";
|
|
if (idx == -1) return false;
|
|
user = cmd.Substring(0, idx);
|
|
try
|
|
{
|
|
user = user.FromBase64String();
|
|
pass = cmd.Substring(idx + 1).FromBase64String();
|
|
}
|
|
catch
|
|
{
|
|
Output.Error($"Recieved problematic username or password! (User: \"{user}\")");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int ParseDataSet(string cmd, out string[] data)
|
|
{
|
|
List<string> gen = new List<string>();
|
|
int idx;
|
|
while ((idx = cmd.IndexOf(':')) != -1)
|
|
{
|
|
try
|
|
{
|
|
gen.Add(cmd.Substring(0, idx).FromBase64String());
|
|
}
|
|
catch
|
|
{
|
|
data = null;
|
|
return -1; // Hard error
|
|
}
|
|
cmd = cmd.Substring(idx + 1);
|
|
}
|
|
try
|
|
{
|
|
gen.Add(cmd.FromBase64String());
|
|
}
|
|
catch
|
|
{
|
|
data = null;
|
|
return -1; // Hard error
|
|
}
|
|
data = gen.ToArray();
|
|
return gen.Count;
|
|
}
|
|
|
|
string[] ParseCommand(string cmd, out long id)
|
|
{
|
|
int idx = cmd.IndexOf(':'), idx1;
|
|
string sub;
|
|
if (idx == -1 || !(sub = cmd.Substring(idx + 1)).Contains(':') || !long.TryParse(sub.Substring(0, idx1 = sub.IndexOf(':')), out id))
|
|
{
|
|
id = 0;
|
|
return null;
|
|
}
|
|
return new string[] { cmd.Substring(0, idx), sub.Substring(idx1 + 1) };
|
|
}
|
|
|
|
string GenerateResponse(long id, dynamic d) => id + ":" + d.ToString();
|
|
string ErrorResponse(long id, string i18n = null) => GenerateResponse(id, $"ERROR{(i18n==null?"":":"+VERBOSE_RESPONSE)}{i18n??""}");
|
|
|
|
bool GetUser(string sid, out Database.User user)
|
|
{
|
|
user = manager.GetUser(sid);
|
|
bool exists = user != null;
|
|
if (exists) user = db.GetUser(user.Name);
|
|
return exists && user!=null;
|
|
}
|
|
|
|
bool GetAccount(string name, Database.User user, out Database.Account acc)
|
|
{
|
|
acc = user.accounts.FirstOrDefault(a => a.name.Equals(name));
|
|
return acc != null;
|
|
}
|
|
|
|
// Create server
|
|
NetServer server = new NetServer(
|
|
EllipticDiffieHellman.Curve25519(EllipticDiffieHellman.Curve25519_GeneratePrivate(random)),
|
|
80,
|
|
(string r, Dictionary<string, string> associations, ref bool s) =>
|
|
{
|
|
string[] cmd = ParseCommand(r, out long id);
|
|
|
|
// Handle corrupt or badly formatted messages from client
|
|
if (cmd == null) return ErrorResponse(-1, "corrupt");
|
|
|
|
// Server endpoints
|
|
switch (cmd[0])
|
|
{
|
|
case "RmUsr":
|
|
{
|
|
if (!GetUser(cmd[1], out var user))
|
|
{
|
|
if(verbosity > 0) Output.Error($"Could not delete user from session as session isn't valid. (SessionID=\"{cmd[1]}\")");
|
|
return ErrorResponse(id, "badsession");
|
|
}
|
|
manager.Expire(user);
|
|
db.RemoveUser(user);
|
|
if (verbosity > 0) Output.Info($"Removed user \"{user.Name}\" (SessionID={cmd[1]})");
|
|
return GenerateResponse(id, true);
|
|
}
|
|
case "Auth": // Log in to a user account (get a session id)
|
|
{
|
|
if(!ParseDataPair(cmd[1], out string user, out string pass))
|
|
{
|
|
if(verbosity > 0) Output.Error($"Recieved problematic username or password! (User: \"{user}\")");
|
|
return ErrorResponse(id);
|
|
}
|
|
Database.User usr = db.GetUser(user);
|
|
if (usr == null || !usr.Authenticate(pass))
|
|
{
|
|
if(verbosity > 0) Output.Error("Authentcation failure for user: "+user);
|
|
return ErrorResponse(id);
|
|
}
|
|
|
|
string sess = manager.GetSession(usr, "ERROR");
|
|
Output.Positive("Authentication success for user: "+user+"\nSession: "+sess);
|
|
associations["session"] = sess;
|
|
return GenerateResponse(id, sess);
|
|
}
|
|
case "Logout": // Prematurely expire a session
|
|
if (manager.Expire(cmd[1])) Output.Info("Prematurely expired session: " + cmd[1]);
|
|
else Output.Error("Attempted to expire a non-existent session!");
|
|
break;
|
|
case "Avail": // Check if the given username is available for account registration
|
|
{
|
|
string name = cmd[1];
|
|
Output.Info($"Performing availability check on name \"{name}\"");
|
|
return GenerateResponse(id, !db.ContainsUser(name));
|
|
}
|
|
case "Refresh": // Refresh a session
|
|
{
|
|
if (verbosity > 0) Output.Info($"Refreshing session \"{cmd[1]}\"");
|
|
return GenerateResponse(id, manager.Refresh(cmd[1]));
|
|
}
|
|
case "List": // List all available users for transactions
|
|
{
|
|
if (!GetUser(cmd[1], out Database.User user))
|
|
{
|
|
if (verbosity > 0) Output.Error("Recieved a bad session id!");
|
|
return ErrorResponse(id, "badsession");
|
|
}
|
|
manager.Refresh(cmd[1]);
|
|
StringBuilder builder = new StringBuilder();
|
|
db.Users(u => { if(u.IsAdministrator || !u.Name.Equals(user)) builder.Append(u.Name.ToBase64String()).Append('&'); return false; });
|
|
if (builder.Length != 0) --builder.Length;
|
|
return GenerateResponse(id, builder);
|
|
}
|
|
case "Info": // Get user info from session data
|
|
{
|
|
if (!GetUser(cmd[1], out var user))
|
|
{
|
|
if (verbosity > 0) Output.Error("Recieved a bad session id!");
|
|
return ErrorResponse(id, "baduser");
|
|
}
|
|
manager.Refresh(cmd[1]);
|
|
StringBuilder builder = new StringBuilder();
|
|
builder.Append(user.Name.ToBase64String()).Append('&').Append(user.IsAdministrator);
|
|
return GenerateResponse(id, builder);
|
|
}
|
|
case "Account_Create": // Create an account
|
|
{
|
|
if (ParseDataSet(cmd[1], out string[] dataset)==-1 || // Get session id and account name
|
|
!GetUser(dataset[0], out var user) || // Get user associated with session id
|
|
GetAccount(dataset[1], user, out var account) || // Check if an account with this name already exists
|
|
!bool.TryParse(dataset[2], out bool checking)) // Check what account type to create
|
|
{
|
|
// Don't print input data to output in case sensitive information was included
|
|
Output.Error($"Failed to create account \"{dataset[1]}\" for user \"{manager.GetUser(dataset[0]).Name}\" (sessionID={dataset[0]})");
|
|
return ErrorResponse(id);
|
|
}
|
|
manager.Refresh(dataset[0]);
|
|
user.accounts.Add(new Database.Account(
|
|
user,
|
|
0,
|
|
dataset[1],
|
|
checking ? Database.Account.AccountType.Checking : Database.Account.AccountType.Savings,
|
|
DateTime.Now.Ticks
|
|
));
|
|
db.UpdateUser(user); // Notify database of the update
|
|
return GenerateResponse(id, true);
|
|
}
|
|
case "Account_Transaction_Create": // Create a transaction from this account (or, if the user is an administrator, add funds to this account)
|
|
{
|
|
bool systemInsert = false;
|
|
string error = VERBOSE_RESPONSE;
|
|
|
|
// Default values used here because compiler can't infer their valid parsing further down
|
|
Database.User user = null;
|
|
Database.Account account = null;
|
|
Database.User tUser = null;
|
|
Database.Account tAccount = null;
|
|
decimal amount = 0;
|
|
|
|
// Expected data (in order): SessionID, AccountName, TargetUserName, TargetAccountName, Amount, [message]
|
|
// Do checks to make sure the data we have been given isn't completely silly
|
|
if (ParseDataSet(cmd[1], out string[] data) < 5 || data.Length > 6)
|
|
error += "general"; // General error (parse failed)
|
|
else if (!GetUser(data[0], out user))
|
|
error += "badsession"; // Bad session id (could not get user from session manager)
|
|
else if (!GetAccount(data[1], user, out account))
|
|
error += "badacc"; // Bad source account name
|
|
else if (!db.ContainsUser(data[2]))
|
|
error += "notargetusr"; // Target user could not be found
|
|
else if (!GetAccount(data[3], tUser = db.GetUser(data[2]), out tAccount))
|
|
error += "notargetacc"; // Target account could not be found
|
|
else if ((systemInsert = (data[2].Equals(user.Name) && account.name.Equals(tAccount.name))) && (!user.IsAdministrator))
|
|
error += "unprivsysins"; // Unprivileged request for system-sourced transfer
|
|
else if (!decimal.TryParse(data[4], out amount) || amount < 0)
|
|
error += "badbalance"; // Given sum was not a valid amount
|
|
else if ((!user.IsAdministrator && !systemInsert && amount > account.ComputeBalance(ratePerDay)))
|
|
error += "insufficient"; // Insufficient funds in the source account
|
|
|
|
// Checks if an error ocurred and handles such a situation appropriately
|
|
if(!error.Equals(VERBOSE_RESPONSE))
|
|
{
|
|
// Don't print input data to output in case sensitive information was included
|
|
Output.Error($"Recieved problematic transaction data ({error}): {data?.ToList().ToReadableString() ?? "Data could not be parsed"}");
|
|
return ErrorResponse(id, error);
|
|
}
|
|
// At this point, we know that all parsed variables above were successfully parsed and valid, therefore: no NREs
|
|
// Parsed vars: 'user', 'account', 'tUser', 'tAccount', 'amount'
|
|
// Perform and log the actual transaction
|
|
manager.Refresh(user);
|
|
return GenerateResponse(id,
|
|
db.AddTransaction(
|
|
systemInsert ? null : user.Name,
|
|
tUser.Name,
|
|
amount,
|
|
account.name,
|
|
tAccount.name,
|
|
ratePerDay,
|
|
data.Length == 6 ? data[5] : null
|
|
));
|
|
}
|
|
case "Account_Close": // Close an account if there is no money on the account
|
|
{
|
|
Database.User user = null;
|
|
Database.Account account = null;
|
|
if (!ParseDataPair(cmd[1], out string session, out string name) || // Get session id and account name
|
|
!GetUser(session, out user) || // Get user associated with session id
|
|
!GetAccount(name, user, out account)/* ||
|
|
account.UncomputedBalance != 0*/) // Get uncomputed balance since computing the balance would mae it impossible to close accounts (deprecated)
|
|
{
|
|
// Don't print input data to output in case sensitive information was included
|
|
Output.Error($"Recieved problematic session id or account name!");
|
|
|
|
// Possible errors: bad session id, bad account name, balance in account isn't 0
|
|
return ErrorResponse(id, (user==null? "badsession" : account==null? "badacc" : "hasbal"));
|
|
}
|
|
manager.Refresh(session);
|
|
user.accounts.Remove(account);
|
|
db.UpdateUser(user); // Update user info
|
|
return GenerateResponse(id, true);
|
|
}
|
|
|
|
case "Account_Get": // Get account info
|
|
{
|
|
Database.User user = null;
|
|
Database.Account account = null;
|
|
if (!ParseDataPair(cmd[1], out string session, out string name) || // Get session id and account name
|
|
!GetUser(session, out user) || // Get user associated with session id
|
|
!GetAccount(name, user, out account))
|
|
{
|
|
// Don't print input data to output in case sensitive information was included
|
|
Output.Error($"Recieved problematic session id or account name!");
|
|
|
|
// Possible errors: bad session id, bad account name, balance in account isn't 0
|
|
return ErrorResponse(id, (user == null ? "badsession" : account == null ? "badacc" : "badmsg"));
|
|
}
|
|
manager.Refresh(session);
|
|
// Response example: "123.45&0{Sm9obiBEb2U=&Sm9obnMgQWNjb3VudA==&SmFuZSBEb2U=&SmFuZXMgQWNjb3VudA==&123.45&SGV5IHRoZXJlIQ=="
|
|
// Exmaple data: balance=123.45, accountType=Savings, Transaction{to="John Doe", toAccount="Johns Account", from="Jane Doe", fromAccount="Janes Account", amount=123.45, meta="Hey there!"}
|
|
return GenerateResponse(id, account.ToString(ratePerDay));
|
|
}
|
|
case "Account_List": // List accounts associated with a certain user (doesn't give more than account names)
|
|
{
|
|
Database.User user = null;
|
|
if(
|
|
ParseDataSet(cmd[1], out var data)!=2 || // Ensure proper dataset is supplied
|
|
!bool.TryParse(data[0], out var bySession) || // Ensure first data point is correctly formatted
|
|
((bySession && !GetUser(data[1], out user)) || (!bySession && (user=db.GetUser(data[1]))==null)) // Get user by session or from database
|
|
)
|
|
{
|
|
if (verbosity > 0) Output.Error($"Recieved errant account list request: {cmd[1]}");
|
|
return ErrorResponse(id); // TODO: Include localization reference
|
|
}
|
|
|
|
// Serialize account names
|
|
StringBuilder builder = new StringBuilder();
|
|
|
|
// Account names are serialized as base64 to prevent potential &'s in the names from causing unintended behaviour
|
|
foreach (var account in user.accounts) builder.Append(account.name.ToBase64String()).Append('&');
|
|
if (builder.Length != 0) --builder.Length;
|
|
|
|
// Return accounts
|
|
return GenerateResponse(id, builder);
|
|
}
|
|
case "Reg": // Register a user
|
|
{
|
|
if (!ParseDataPair(cmd[1], out string user, out string pass))
|
|
{
|
|
// Don't print input data to output in case sensitive information was included
|
|
if(verbosity > 0) Output.Error($"Recieved problematic username or password!");
|
|
return ErrorResponse(id, "userpass");
|
|
}
|
|
|
|
// Cannot register an account with an existing username
|
|
if (db.ContainsUser(user))
|
|
{
|
|
if (verbosity > 0) Output.Error("Caught attempt to register existing account");
|
|
return ErrorResponse(id, "exists");
|
|
}
|
|
|
|
// Create the database user entry and generate a personal password salt
|
|
Database.User u = new Database.User(user, pass, random.GetBytes(Math.Abs(random.NextShort() % 60) + 20), true);
|
|
db.AddUser(u);
|
|
|
|
// Generate a session token
|
|
string sess = manager.GetSession(u, "ERROR");
|
|
if(verbosity > 0) Output.Positive("Registered account: " + u.Name + "\nSession: "+sess);
|
|
associations["session"] = sess;
|
|
return GenerateResponse(id, sess);
|
|
}
|
|
case "PassUPD": // Update password for a certain user
|
|
{
|
|
if(!ParseDataPair(cmd[1], out var session, out var pass))
|
|
{
|
|
if (verbosity == 2) Output.Error("Recieved faulty data from client attempting to update password!");
|
|
return ErrorResponse(id);
|
|
}
|
|
if(!GetUser(session, out var user))
|
|
{
|
|
if (verbosity > 0) Output.Error("Recieved faulty session id: "+session);
|
|
return ErrorResponse(id, "badsession");
|
|
}
|
|
manager.Refresh(session);
|
|
user.Salt = Convert.ToBase64String(random.GetBytes(Math.Abs(random.NextShort() % 60) + 20));
|
|
user.PasswordHash = user.ComputePass(pass);
|
|
db.UpdateUser(user);
|
|
return GenerateResponse(id, true);
|
|
}
|
|
case "Verify": // Verifies server identity
|
|
{
|
|
BitReader bd = new BitReader(Convert.FromBase64String(cmd[1]));
|
|
try
|
|
{
|
|
while (!t.IsCompleted && !t.IsFaulted) System.Threading.Thread.Sleep(75);
|
|
if (t.IsFaulted)
|
|
{
|
|
Output.Fatal("Encountered error when getting RSA keyset:\n"+t.Exception.ToString());
|
|
return ErrorResponse(id, "server_err");
|
|
}
|
|
byte[] ser;
|
|
using (BitWriter collector = new BitWriter())
|
|
{
|
|
// Serialize public component of RSA keyset
|
|
collector.PushArray(t.Result.Serialize());
|
|
|
|
// Prove server identity by signing a nonce with RSA
|
|
collector.PushArray(t.Result.Encrypt(((BigInteger)bd.ReadUShort()).ToByteArray(), null, true));
|
|
ser = collector.Finalize();
|
|
}
|
|
return GenerateResponse(id, Convert.ToBase64String(ser));
|
|
}
|
|
catch
|
|
{
|
|
Output.Error("Cryptographic verification failed!");
|
|
return ErrorResponse(id, "crypterr");
|
|
}
|
|
}
|
|
default: // The given message could not be matched to any known endpoint
|
|
return ErrorResponse(id, "unwn"); // Unknown request
|
|
}
|
|
|
|
return null; // Don't respond to client
|
|
},
|
|
(c, b) => // Called every time a client connects or disconnects (conn + dc with every command/request)
|
|
{
|
|
if(verbosity>1) Output.Info($"{(b?"Accepted":"Dropped")} connection {(b?"from":"to")} {c.Remote.Address.ToString()}:{c.Remote.Port}");
|
|
// Output.Info($"Client has {(b ? "C" : "Disc")}onnected");
|
|
//if(!b && c.assignedValues.ContainsKey("session"))
|
|
// manager.Expire(c.assignedValues["session"]);
|
|
});
|
|
server.StartListening();
|
|
|
|
|
|
bool running = true;
|
|
|
|
// Create the command manager
|
|
CommandHandler commands = null;
|
|
commands =
|
|
new CommandHandler(4, " ", "", "- ")
|
|
.Append(new Command("help")
|
|
.SetAction(() => Output.Raw("Available commands:\n" + commands.GetString())), "Show this help menu") // Show help menu
|
|
.Append(new Command("stop").SetAction(() => running = false), "Stop server") // Stop server
|
|
.Append(new Command("clear").SetAction(() => Output.Clear()), "Clear screen") // Clear screen
|
|
.Append(new Command("verb").WithParameter("level", 'l', Parameter.ParamType.STRING, true).SetAction((c, l) => // Set output verbosity
|
|
{
|
|
if (l.Count == 1)
|
|
{
|
|
string level = l[0].Item1;
|
|
if (level.EqualsIgnoreCase("debug") || level.Equals("0")) verbosity = 2;
|
|
else if (level.EqualsIgnoreCase("info") || level.Equals("1")) verbosity = 1;
|
|
else if (level.EqualsIgnoreCase("fatal") || level.Equals("2")) verbosity = 0;
|
|
else Output.Error($"Unknown verbosity level supplied!\nusage: {c.CommandString}");
|
|
}
|
|
Output.Raw($"Current verbosity level: {(verbosity<1?"FATAL":verbosity==1?"INFO":"DEBUG")}");
|
|
}), "Get or set verbosity level: DEBUG, INFO, FATAL (alternatively enter 0, 1 or 2 respectively)")
|
|
.Append(new Command("sess") // Display active sessions
|
|
.WithParameter("sessionID", 'r', Parameter.ParamType.STRING, true)
|
|
.WithParameter("username", 'u', Parameter.ParamType.STRING, true)
|
|
.SetAction(
|
|
(c, l) => {
|
|
StringBuilder builder = new StringBuilder();
|
|
manager.Update(); // Ensure that we don't show expired sessions (artifacts exist until it is necessary to remove them)
|
|
bool r = l.HasFlag('r'), u = l.HasFlag('u');
|
|
if(r && u)
|
|
{
|
|
Output.Error("Cannot refresh session by username AND SessionID!");
|
|
return;
|
|
}
|
|
if((r||u) && !manager.HasSession(l[0].Item1, u))
|
|
{
|
|
Output.Error("Session could not be found!");
|
|
return;
|
|
}
|
|
if (r||u) Output.Raw($"Session refreshed: {manager.Refresh(l[0].Item1, u)}");
|
|
else
|
|
{
|
|
foreach (var session in manager.Sessions)
|
|
builder
|
|
.Append(session.user.Name)
|
|
.Append(" : ")
|
|
.Append(session.sessionID)
|
|
.Append(" : ")
|
|
.Append((session.expiry - DateTime.Now.Ticks) / TimeSpan.TicksPerMillisecond)
|
|
.Append('\n');
|
|
if (builder.Length == 0) builder.Append("There are no active sessions at the moment");
|
|
else --builder.Insert(0, "Active sessions:\n").Length;
|
|
Output.Raw(builder);
|
|
}
|
|
}), "List or refresh active client sessions")
|
|
.Append(new Command("list").WithParameter(Parameter.Flag('a')).SetAction( // Display users
|
|
(c, l) => {
|
|
bool filter = l.HasFlag('a');
|
|
StringBuilder builder = new StringBuilder();
|
|
foreach (var user in db.Users(u => !filter || (filter && u.IsAdministrator)))
|
|
builder.Append(user.Name).Append('\n');
|
|
if (builder.Length != 0)
|
|
{
|
|
builder.Length = builder.Length - 1;
|
|
Output.Raw(builder);
|
|
}
|
|
}), "Show registered users. Add \"-a\" to only list admins")
|
|
.Append(new Command("admin") // Give or revoke administrator privileges for a certain user
|
|
.WithParameter("username", 'u', Parameter.ParamType.STRING) // Guaranteed to appear in the list passed in the action
|
|
.WithParameter("true/false", 's', Parameter.ParamType.BOOLEAN, true) // Might show up
|
|
.SetAction(
|
|
(c, l) =>
|
|
{
|
|
bool set = l.HasFlag('s');
|
|
string username = l.GetFlag('u');
|
|
Database.User user = db.GetUser(username);
|
|
if (user == null) {
|
|
Output.RawErr($"User \"{username}\" could not be found in the databse!");
|
|
return;
|
|
}
|
|
if (set)
|
|
{
|
|
bool admin = bool.Parse(l.GetFlag('s'));
|
|
if (user.IsAdministrator == admin) Output.Info("The given administrator state was already set");
|
|
else if (admin) Output.Raw("User is now an administrator");
|
|
else Output.Raw("User is no longer an administrator");
|
|
user.IsAdministrator = admin;
|
|
db.AddUser(user);
|
|
}
|
|
else Output.Raw(user.IsAdministrator);
|
|
}), "Show or set admin status for a user")
|
|
.Append(new Command("flush").SetAction(() => { db.Flush(); Output.Raw("Database flushed"); }), "Flush database");// Flush database to database file
|
|
|
|
// Set up a persistent terminal-esque input design
|
|
Output.OnNewLine = () => Output.WriteOverwritable(">> ");
|
|
Output.Raw(CONSOLE_MOTD);
|
|
Output.Raw($"Rent rate set to: {ratePerDay} (growth of {(1+ratePerDay)*100}% per day)");
|
|
Output.OnNewLine();
|
|
|
|
// Server command loop
|
|
while (running)
|
|
{
|
|
// Handle command input
|
|
if (!commands.HandleCommand(Output.ReadLine()))
|
|
Output.Error("Unknown command. Enter 'help' for a list of supported commands.", true, false);
|
|
}
|
|
|
|
// Stop the server (obviously)
|
|
server.StopRunning();
|
|
|
|
// Flush database
|
|
//db.Flush();
|
|
}
|
|
|
|
// Handles unexpected console close events (kernel event hook for window close event)
|
|
private delegate bool EventHandler(int eventType);
|
|
[DllImport("Kernel32.dll", SetLastError = true)]
|
|
private static extern bool SetConsoleCtrlHandler(EventHandler callback, bool add);
|
|
}
|
|
}
|