* Added accounts types (savings and checking) * FS flush optimized balance computation * Automatic daily rate growth computations
554 lines
32 KiB
C#
554 lines
32 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
|
|
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);
|
|
|
|
// Initialize the database
|
|
Database db = new Database("BankDB", "Resources");
|
|
SetConsoleCtrlHandler(i => {
|
|
if (i == 2) 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)
|
|
.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)
|
|
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")]
|
|
private static extern bool SetConsoleCtrlHandler(EventHandler callback, bool add);
|
|
}
|
|
}
|