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
This commit is contained in:
Gabriel Tofvesson 2018-05-02 22:26:19 +02:00
parent 98a6557000
commit eeaf8c708f
9 changed files with 322 additions and 105 deletions

View File

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

View File

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

View File

@ -10,4 +10,16 @@
</Options>
<Text>@string/ERR_empty</Text>
</DialogView>
<DialogView id="ConnectionError"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
border="4">
<Options>
<Option>@string/GENERIC_accept</Option>
</Options>
<Text>@string/NC_connerr</Text>
</DialogView>
</Resources>

View File

@ -69,16 +69,4 @@
</Options>
<Text>@string/NC_porterr</Text>
</DialogView>
<DialogView id="ConnectionError"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
border="4">
<Options>
<Option>@string/GENERIC_accept</Option>
</Options>
<Text>@string/NC_connerr</Text>
</DialogView>
</Elements>

166
Server/Command.cs Normal file
View File

@ -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<Command, List<Tuple<string, Parameter>>> OnInvoke { get; set; }
private List<Parameter> parameters = new List<Parameter>();
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<Command, List<Tuple<string, Parameter>>> 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<Tuple<string, Parameter>> p = new List<Tuple<string, Parameter>>();
StringBuilder reconstruct = new StringBuilder();
Parameter? p1 = null;
bool wasFlag = true;
for (int i = 1; i<parts.Length; ++i)
{
if (parts[i].StartsWith("-") && parts[i].Length==2)
{
if (reconstruct.Length != 0)
{
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<string, Parameter>(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<string, Parameter>(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<Tuple<string, Parameter>> l, char flag)
{
foreach (var flagcheck in l)
if (flagcheck.Item2.flag == flag)
return flagcheck.Item1;
return null;
}
public static bool HasFlag(this List<Tuple<string, Parameter>> l, char flag)
{
foreach (var flagcheck in l)
if (flagcheck.Item2.flag == flag)
return true;
return false;
}
}
}

View File

@ -6,15 +6,15 @@ using System.Threading.Tasks;
namespace Server
{
public sealed class OutputFormatter
public sealed class CommandHandler
{
private readonly List<Tuple<string, string>> lines = new List<Tuple<string, string>>();
private readonly List<Tuple<Command, string>> commands = new List<Tuple<Command, string>>();
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<string, string>(key, value));
leftLen = Math.Max(key.Length + minPad, leftLen);
commands.Add(new Tuple<Command, string>(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();
}

22
Server/Parameter.cs Normal file
View File

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

View File

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

View File

@ -43,9 +43,11 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Command.cs" />
<Compile Include="Database.cs" />
<Compile Include="Output.cs" />
<Compile Include="OutputFormatter.cs" />
<Compile Include="CommandHandler.cs" />
<Compile Include="Parameter.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs">