From eeaf8c708f4d663f6e38cd4c999002c3beb4a35c Mon Sep 17 00:00:00 2001 From: GabrielTofvesson Date: Wed, 2 May 2018 22:26:19 +0200 Subject: [PATCH] Revised and refactored OutputFormatter - OutputFormatter is now renamed to CommandHandler CommandHandler now supports full command management - Dynamically generates command list (for "help") - Added Command class which specifies the structure of a command - Added struct specifying the structure of a command parameter Added exception handling to networking Moved shared layout resource to Common layout file --- Client/Context/NetContext.cs | 13 +- Client/Context/WelcomeContext.cs | 24 ++- Client/Resources/Layout/Common.xml | 12 ++ Client/Resources/Layout/Networking.xml | 12 -- Server/Command.cs | 166 ++++++++++++++++++ .../{OutputFormatter.cs => CommandHandler.cs} | 31 ++-- Server/Parameter.cs | 22 +++ Server/Program.cs | 143 +++++++-------- Server/Server.csproj | 4 +- 9 files changed, 322 insertions(+), 105 deletions(-) create mode 100644 Server/Command.cs rename Server/{OutputFormatter.cs => CommandHandler.cs} (54%) create mode 100644 Server/Parameter.cs diff --git a/Client/Context/NetContext.cs b/Client/Context/NetContext.cs index dade161..2e29b67 100644 --- a/Client/Context/NetContext.cs +++ b/Client/Context/NetContext.cs @@ -57,8 +57,17 @@ namespace Client return; } */ - - Promise verify = Promise.AwaitPromise(ita.CheckIdentity(new RSA(Resources.e_0x100, Resources.n_0x100), provider.NextUShort())); + + Promise verify; + try + { + verify = Promise.AwaitPromise(ita.CheckIdentity(new RSA(Resources.e_0x100, Resources.n_0x100), provider.NextUShort())); + } + catch + { + Show("ConnectionError"); + return; + } verify.Subscribe = p => { diff --git a/Client/Context/WelcomeContext.cs b/Client/Context/WelcomeContext.cs index 68a87fc..4117d96 100644 --- a/Client/Context/WelcomeContext.cs +++ b/Client/Context/WelcomeContext.cs @@ -44,7 +44,16 @@ namespace Client { // Authenticate against server here Show("AuthWait"); - promise = Promise.AwaitPromise(interactor.Authenticate(i.Inputs[0].Text, i.Inputs[1].Text)); + try + { + promise = Promise.AwaitPromise(interactor.Authenticate(i.Inputs[0].Text, i.Inputs[1].Text)); + } + catch + { + Hide("AuthWait"); + Show("ConnectionError"); + return; + } //promise = prom.Result; promise.Subscribe = response => @@ -90,12 +99,21 @@ namespace Client void a() { Show("RegWait"); - promise = Promise.AwaitPromise(interactor.Register(i.Inputs[0].Text, i.Inputs[1].Text)); + try + { + promise = Promise.AwaitPromise(interactor.Register(i.Inputs[0].Text, i.Inputs[1].Text)); + } + catch + { + Hide("RegWait"); + Show("ConnectionError"); + return; + } promise.Subscribe = response => { Hide("RegWait"); - if (response.Value.Equals("ERROR")) + if (response.Value.StartsWith("ERROR")) Show("DuplicateAccountError"); else { diff --git a/Client/Resources/Layout/Common.xml b/Client/Resources/Layout/Common.xml index 31594e9..8ef8725 100644 --- a/Client/Resources/Layout/Common.xml +++ b/Client/Resources/Layout/Common.xml @@ -10,4 +10,16 @@ @string/ERR_empty + + + + + + @string/NC_connerr + \ No newline at end of file diff --git a/Client/Resources/Layout/Networking.xml b/Client/Resources/Layout/Networking.xml index 8a2279e..119de52 100644 --- a/Client/Resources/Layout/Networking.xml +++ b/Client/Resources/Layout/Networking.xml @@ -69,16 +69,4 @@ @string/NC_porterr - - - - - - @string/NC_connerr - \ No newline at end of file diff --git a/Server/Command.cs b/Server/Command.cs new file mode 100644 index 0000000..277bf1a --- /dev/null +++ b/Server/Command.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Tofvesson.Crypto; + +namespace Server +{ + public sealed class Command + { + public string Name { get; } + public Action>> OnInvoke { get; set; } + private List parameters = new List(); + + public string CommandString + { + get + { + StringBuilder builder = new StringBuilder(Name); + foreach (var p in parameters) + { + builder.Append(' '); + if (p.optional) builder.Append('{'); + builder.Append('-').Append(p.flag); + if (p.type != Parameter.ParamType.NONE) builder.Append(' ').Append(p.name); + if (p.optional) builder.Append('}'); + } + return builder.ToString(); + } + } + + + public Command(string name) => Name = name; + + public Command WithParameter(string pName, char flag, Parameter.ParamType type, bool optional = false) + { + if (GetByFlag(flag) != null) throw new Exception("Cannot have two parameters with the same flag"); + parameters.Add(new Parameter(pName, flag, type, optional)); + return this; + } + + public Command WithParameter(Parameter parameter) + { + if (GetByFlag(parameter.flag) != null) throw new Exception("Cannot have two parameters with the same flag"); + parameters.Add(parameter); + return this; + } + + public Command SetAction(Action>> action) + { + OnInvoke = action; + return this; + } + public Command SetAction(Action a) => SetAction((_, __) => a?.Invoke()); + + public bool Matches(string cmd) => cmd.Split(' ')[0].EqualsIgnoreCase(Name); + + public Parameter? GetByFlag(char flag) + { + foreach (var param in parameters) + if (param.flag == flag) + return param; + return null; + } + + public bool Invoke(string cmd) + { + if (!Matches(cmd)) return false; + string[] parts = cmd.Split(' '); + List> p = new List>(); + StringBuilder reconstruct = new StringBuilder(); + Parameter? p1 = null; + bool wasFlag = true; + for (int i = 1; i(reconstruct.ToString(), p1.Value)); + reconstruct.Length = 0; + wasFlag = true; + } + if((p1 = GetByFlag(parts[i][1])) == null) + { + ShowError(); + return false; + } + } + else + { + if(p1!=null && p1.Value.type == Parameter.ParamType.NONE) + { + ShowError(); + return false; + } + if (!wasFlag) reconstruct.Append(' '); + reconstruct.Append(parts[i]); + wasFlag = false; + } + } + + if (reconstruct.Length != 0 || (p1 != null && !p.HasFlag(p1.Value.flag))) + { + if ( + p1 == null || + (p1.Value.type == Parameter.ParamType.NUMBER && !double.TryParse(reconstruct.ToString(), out double _)) || + (p1.Value.type == Parameter.ParamType.BOOLEAN && !bool.TryParse(reconstruct.ToString(), out bool _)) + ) + { + ShowError(); + return true; + } + p.Add(new Tuple(reconstruct.ToString(), p1.Value)); + reconstruct.Length = 0; + } + + foreach (var check in parameters) + if (check.optional) continue; + else + { + foreach (var check1 in p) + if (check1.Item2.Equals(check)) + goto found; + // Could not find a match for a required parameter + ShowError(); + return false; + + found: { } + } + OnInvoke?.Invoke(this, p); + return true; + } + + public void ShowError() => Output.Error($"Usage: {CommandString}"); + } + + public static class Commands + { + public static string GetFlag(this List> l, char flag) + { + foreach (var flagcheck in l) + if (flagcheck.Item2.flag == flag) + return flagcheck.Item1; + return null; + } + + public static bool HasFlag(this List> l, char flag) + { + foreach (var flagcheck in l) + if (flagcheck.Item2.flag == flag) + return true; + return false; + } + } +} diff --git a/Server/OutputFormatter.cs b/Server/CommandHandler.cs similarity index 54% rename from Server/OutputFormatter.cs rename to Server/CommandHandler.cs index ef7d7f8..e7b917a 100644 --- a/Server/OutputFormatter.cs +++ b/Server/CommandHandler.cs @@ -6,15 +6,15 @@ using System.Threading.Tasks; namespace Server { - public sealed class OutputFormatter + public sealed class CommandHandler { - private readonly List> lines = new List>(); + private readonly List> commands = new List>(); private int leftLen = 0; private readonly int minPad; private readonly string prepend, delimiter, postpad, trail; - public OutputFormatter(int minPad = 1, string prepend = "", string delimiter = "", string postpad = "", string trail = "") + public CommandHandler(int minPad = 1, string prepend = "", string delimiter = "", string postpad = "", string trail = "") { this.prepend = prepend; this.delimiter = delimiter; @@ -23,27 +23,36 @@ namespace Server this.minPad = Math.Abs(minPad); } - public OutputFormatter Append(string key, string value) + public CommandHandler Append(Command c, string description) { - lines.Add(new Tuple(key, value)); - leftLen = Math.Max(key.Length + minPad, leftLen); + commands.Add(new Tuple(c, description)); + leftLen = Math.Max(c.CommandString.Length + minPad, leftLen); return this; } + public bool HandleCommand(string cmd) + { + foreach (var command in commands) + if (command.Item1.Invoke(cmd)) + return true; + return false; + } + public string GetString() { StringBuilder builder = new StringBuilder(); - foreach (var line in lines) + string cache; + foreach (var command in commands) builder .Append(prepend) - .Append(line.Item1) + .Append(cache = command.Item1.CommandString) .Append(delimiter) - .Append(Pad(line.Item1, leftLen)) + .Append(Pad(cache, leftLen)) .Append(postpad) - .Append(line.Item2) + .Append(command.Item2) .Append(trail) .Append('\n'); - builder.Length -= 1; + if(commands.Count > 0) builder.Length -= 1; return builder.ToString(); } diff --git a/Server/Parameter.cs b/Server/Parameter.cs new file mode 100644 index 0000000..d2432e4 --- /dev/null +++ b/Server/Parameter.cs @@ -0,0 +1,22 @@ +namespace Server +{ + public struct Parameter + { + public enum ParamType { STRING, NUMBER, BOOLEAN, NONE } + public readonly ParamType type; + public readonly string name; + public readonly char flag; + public readonly bool optional; + + public Parameter(string name, char flag, ParamType type, bool optional = false) + { + this.name = name; + this.flag = flag; + this.type = type; + this.optional = optional; + } + + // Easy shortcut to create parameterless flags + public static Parameter Flag(char flagChar, bool optional = true) => new Parameter("", flagChar, ParamType.NONE, optional); + } +} diff --git a/Server/Program.cs b/Server/Program.cs index 47f1ffb..f1941fa 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -104,6 +104,7 @@ namespace Server } 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) { @@ -133,13 +134,13 @@ namespace Server if(!ParseDataPair(cmd[1], out string user, out string pass)) { Output.Error($"Recieved problematic username or password! (User: \"{user}\")"); - return GenerateResponse(id, "ERROR"); + return ErrorResponse(id); } Database.User usr = db.GetUser(user); if (usr == null || !usr.Authenticate(pass)) { Output.Error("Authentcation failure for user: "+user); - return GenerateResponse(id, "ERROR"); + return ErrorResponse(id); } string sess = manager.GetSession(usr, "ERROR"); @@ -173,7 +174,7 @@ namespace Server { // Don't print input data to output in case sensitive information was included Output.Error($"Recieved problematic session id or account name!"); - return GenerateResponse(id, "ERROR"); + return ErrorResponse(id); } user.accounts.Add(new Database.Account(user, 0, name)); db.UpdateUser(user); // Notify database of the update @@ -215,7 +216,7 @@ namespace Server { // Don't print input data to output in case sensitive information was included Output.Error($"Recieved problematic transaction data ({error}): {data?.ToList().ToString() ?? "Data could not be parsed"}"); - return GenerateResponse(id, $"ERROR:{error}"); + 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' @@ -243,7 +244,7 @@ namespace Server Output.Error($"Recieved problematic session id or account name!"); // Possible errors: bad session id, bad account name, balance in account isn't 0 - return GenerateResponse(id, $"ERROR:{VERBOSE_RESPONSE} {(user==null? "badsession" : account==null? "badacc" : "hasbal")}"); + return ErrorResponse(id, (user==null? "badsession" : account==null? "badacc" : "hasbal")); } break; } @@ -253,11 +254,11 @@ namespace Server { // Don't print input data to output in case sensitive information was included Output.Error($"Recieved problematic username or password!"); - return GenerateResponse(id, $"ERROR:{VERBOSE_RESPONSE}userpass"); + return ErrorResponse(id, "userpass"); } // Cannot register an account with an existing username - if (db.ContainsUser(user)) return GenerateResponse(id, $"ERROR:{VERBOSE_RESPONSE}exists"); + if (db.ContainsUser(user)) 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); @@ -286,11 +287,11 @@ namespace Server } catch { - return GenerateResponse(id, $"ERROR:{VERBOSE_RESPONSE}crypterr"); + return ErrorResponse(id, "crypterr"); } } default: - return GenerateResponse(id, $"ERROR:{VERBOSE_RESPONSE}unwn"); // Unknown request + return ErrorResponse(id, "unwn"); // Unknown request } return null; @@ -304,81 +305,71 @@ namespace Server server.StartListening(); - string commands = - new OutputFormatter(4, " ", "", "- ") - .Append("help", "Show this help menu") - .Append("stop", "Stop server") - .Append("sessions", "Show active client sessions") - .Append("list {admin}", "Show registered users. Add \"admin\" to only list admins") - .Append("admin [user] {true/false}", "Show or set admin status for a user") - .GetString(); - - Output.OnNewLine = () => Output.WriteOverwritable(">> "); - Output.OnNewLine(); - // Server command loop - while (true) - { - string cmd = Output.ReadLine(); - string[] parts = cmd.Split(); - - if (cmd.EqualsIgnoreCase("stop")) break; - else if (cmd.EqualsIgnoreCase("sessions")) - { - 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('\n'); - if (builder.Length == 0) builder.Append("There are no active sessions at the moment"); - else builder.Length = builder.Length - 1; - Output.Raw(builder); - } - else if (parts[0].EqualsIgnoreCase("admin")) - { - if (parts.Length == 1) Output.Raw("Usage: admin [username] {true/false}"); - else if (parts.Length == 2) - { - Database.User user = db.GetUser(parts[1]); - if (user == null) Output.RawErr($"User \"{parts[1]}\" could not be found in the databse!"); - else Output.Raw(user.IsAdministrator); - } - else if (parts.Length == 3) - { - Database.User user = db.GetUser(parts[1]); - if (user == null) Output.RawErr($"User \"{parts[1]}\" could not be found in the databse!"); - else if (!bool.TryParse(parts[2].ToLower(), out bool admin)) Output.RawErr($"Could not interpret \"{parts[2]}\""); - else - { - 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.RawErr("Too many parameters!"); - } - else if (parts[0].EqualsIgnoreCase("list")) - { - if (parts.Length > 2) Output.RawErr("Too many parameters!"); - else - { - bool filter = parts.Length > 1, filterAdmin = filter && parts[1].EqualsIgnoreCase("admin"); + 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") + .Append(new Command("stop").SetAction(() => running = false), "Stop server") + .Append(new Command("sess").SetAction( + (c, l) => { StringBuilder builder = new StringBuilder(); - foreach (var user in db.Users(u => !filter || (filterAdmin && u.IsAdministrator))) + 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('\n'); + if (builder.Length == 0) builder.Append("There are no active sessions at the moment"); + else builder.Length = builder.Length - 1; + Output.Raw(builder); + }), "Show active client sessions") + .Append(new Command("list").WithParameter(Parameter.Flag('a')).SetAction( + (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); } - } - } - else if (cmd.EqualsIgnoreCase("help")) - { - Output.Raw("Available commands:\n" + commands); - } - else if (cmd.Length != 0) Output.RawErr("Unknown command. Use command \"help\" to view available commands"); + }), "Show registered users. Add \"-a\" to only list admins") + .Append(new Command("admin") + .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"); + + // Set up a persistent terminal-esque input design + Output.OnNewLine = () => Output.WriteOverwritable(">> "); + 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); } server.StopRunning(); diff --git a/Server/Server.csproj b/Server/Server.csproj index 058329b..45b16ce 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -43,9 +43,11 @@ + - + +