Massive update
* Added new endpoint for updating password * Added internationalization method to ContextManager and Context * Updated contexts to use internationalization * Added a fancy text-based UI to the server * Added translations * Moved Promise class to its own file * Made BankNetInteractor its own file * Added a lot of convenient methods * Added many more comments * Fixed input event management in ButtonView * Added support for dynamic ListView content modification * Added more layouts * Fixed some namespaces * Added more commands to the server
This commit is contained in:
parent
f6c7fa83bb
commit
bdbb1342ba
@ -1,14 +1,13 @@
|
||||
using Common;
|
||||
using Common.Cryptography.KeyExchange;
|
||||
using Tofvesson.Common;
|
||||
using Tofvesson.Common.Cryptography.KeyExchange;
|
||||
using Tofvesson.Net;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Tofvesson.Common;
|
||||
using Tofvesson.Crypto;
|
||||
using System.Text;
|
||||
|
||||
namespace Client
|
||||
{
|
||||
@ -17,7 +16,7 @@ namespace Client
|
||||
protected static readonly CryptoRandomProvider provider = new CryptoRandomProvider();
|
||||
protected static readonly Dictionary<long, OnClientConnectStateChanged> changeListeners = new Dictionary<long, OnClientConnectStateChanged>();
|
||||
|
||||
protected Dictionary<long, Promise> promises = new Dictionary<long, Promise>();
|
||||
protected Dictionary<long, Tuple<Promise, Common.Proxy<bool>>> promises = new Dictionary<long, Tuple<Promise, Common.Proxy<bool>>>();
|
||||
protected NetClient client;
|
||||
protected readonly IPAddress addr;
|
||||
protected readonly short port;
|
||||
@ -33,6 +32,9 @@ namespace Client
|
||||
}
|
||||
protected long loginTimeout = -1;
|
||||
protected string sessionID = null;
|
||||
public string UserSession { get => sessionID; }
|
||||
protected Task sessionChecker;
|
||||
public bool RefreshSessions { get; set; }
|
||||
|
||||
|
||||
public BankNetInteractor(string address, short port)
|
||||
@ -40,8 +42,14 @@ namespace Client
|
||||
this.addr = IPAddress.Parse(address);
|
||||
this.port = port;
|
||||
this.keyExchange = EllipticDiffieHellman.Curve25519(EllipticDiffieHellman.Curve25519_GeneratePrivate(provider));
|
||||
RefreshSessions = true; // Default is to auto-refresh sessions
|
||||
}
|
||||
|
||||
protected async Task StatusCheck(bool doLoginCheck = false)
|
||||
{
|
||||
if (doLoginCheck && !IsLoggedIn) throw new SystemException("Not logged in");
|
||||
await Connect();
|
||||
}
|
||||
protected virtual async Task Connect()
|
||||
{
|
||||
if (IsAlive) return;
|
||||
@ -79,10 +87,12 @@ namespace Client
|
||||
{
|
||||
string response = HandleResponse(msg, out long pID, out bool err);
|
||||
if (err || !promises.ContainsKey(pID)) return null;
|
||||
Promise p = promises[pID];
|
||||
var t = promises[pID];
|
||||
promises.Remove(pID);
|
||||
if (t.Item2) return null; // Promise has been canceled
|
||||
var p = t.Item1;
|
||||
PostPromise(p, response);
|
||||
if (promises.Count == 0) keepAlive = false;
|
||||
if (promises.Count == 0) keepAlive = false; // If we aren't awaiting any other promises, disconnect from server
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -94,53 +104,112 @@ namespace Client
|
||||
|
||||
public async virtual Task<Promise> CheckAccountAvailability(string username)
|
||||
{
|
||||
await Connect();
|
||||
await StatusCheck();
|
||||
if (username.Length > 60)
|
||||
return new Promise
|
||||
{
|
||||
HasValue = true,
|
||||
Value = "ERROR"
|
||||
};
|
||||
client.Send(CreateCommandMessage("Avail", username.ToBase64String(), out long pID));
|
||||
client.Send(CreateCommandMessage("Avail", username, out long pID));
|
||||
return RegisterPromise(pID);
|
||||
}
|
||||
|
||||
public async virtual Task<Promise> Authenticate(string username, string password)
|
||||
public async virtual Task<Promise> Authenticate(string username, string password, bool autoRefresh = true)
|
||||
{
|
||||
await Connect();
|
||||
await StatusCheck();
|
||||
if (username.Length > 60)
|
||||
return new Promise
|
||||
{
|
||||
HasValue = true,
|
||||
Value = "ERROR"
|
||||
};
|
||||
client.Send(CreateCommandMessage("Auth", username.ToBase64String()+":"+password.ToBase64String(), out long pID));
|
||||
client.Send(CreateCommandMessage("Auth", DataSet(username, password), out long pID));
|
||||
|
||||
return RegisterEventPromise(pID, p =>
|
||||
{
|
||||
bool b = !p.Value.StartsWith("ERROR");
|
||||
PostPromise(p.handler, b);
|
||||
if (b)
|
||||
if (b) // Set proper state before notifying listener
|
||||
{
|
||||
loginTimeout = 280 * TimeSpan.TicksPerSecond;
|
||||
sessionID = p.Value;
|
||||
}
|
||||
PostPromise(p.handler, b);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public async virtual Task<Promise> UpdatePassword(string newPass)
|
||||
{
|
||||
await StatusCheck(true);
|
||||
client.Send(CreateCommandMessage("PassUPD", DataSet(sessionID, newPass), out var pID));
|
||||
return RegisterEventPromise(pID, p =>
|
||||
{
|
||||
bool noerror = !p.Value.StartsWith("ERROR");
|
||||
if (noerror) // Set proper state before notifying listener
|
||||
{
|
||||
loginTimeout = 280 * TimeSpan.TicksPerSecond;
|
||||
sessionID = p.Value;
|
||||
}
|
||||
PostPromise(p.handler, noerror);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public async virtual Task<Promise> ListUserAccounts() => await ListAccounts(sessionID, true);
|
||||
public async virtual Task<Promise> ListAccounts(string username) => await ListAccounts(username, false);
|
||||
protected async virtual Task<Promise> ListAccounts(string username, bool bySession)
|
||||
{
|
||||
await StatusCheck();
|
||||
client.Send(CreateCommandMessage("Account_List", DataSet(bySession.ToString(), username), out long PID));
|
||||
return RegisterPromise(PID);
|
||||
}
|
||||
|
||||
public async virtual Task<Promise> UserInfo()
|
||||
{
|
||||
await StatusCheck(true);
|
||||
client.Send(CreateCommandMessage("Info", sessionID, out long PID));
|
||||
return RegisterPromise(PID);
|
||||
}
|
||||
|
||||
public async virtual Task<Promise> AccountInfo(string accountName)
|
||||
{
|
||||
await StatusCheck();
|
||||
client.Send(CreateCommandMessage("Account_Get", DataSet(sessionID, accountName), out var pID));
|
||||
return RegisterPromise(pID);
|
||||
}
|
||||
|
||||
public async virtual Task<Promise> CreateTransaction(string fromAccount, string targetUser, string targetAccount, decimal amount, string message = null)
|
||||
{
|
||||
await StatusCheck(true);
|
||||
client.Send(CreateCommandMessage("Account_Transaction_Create", DataSet(sessionID, fromAccount, targetUser, targetAccount, amount.ToString(), message), out var pID));
|
||||
RefreshTimeout();
|
||||
return RegisterPromise(pID);
|
||||
}
|
||||
|
||||
public async virtual Task<Promise> CloseAccount(string accountName)
|
||||
{
|
||||
await StatusCheck(true);
|
||||
client.Send(CreateCommandMessage("Account_Close", DataSet(sessionID, accountName), out var pID));
|
||||
RefreshTimeout();
|
||||
return RegisterEventPromise(pID, p =>
|
||||
{
|
||||
p.handler.Value = p.Value.StartsWith("ERROR").ToString();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public async virtual Task<Promise> CreateAccount(string accountName)
|
||||
{
|
||||
if (!IsLoggedIn) throw new SystemException("Not logged in");
|
||||
await Connect();
|
||||
client.Send(CreateCommandMessage("Account_Create", $"{sessionID}:{accountName}", out long PID));
|
||||
await StatusCheck(true);
|
||||
client.Send(CreateCommandMessage("Account_Create", DataSet(sessionID, accountName), out long PID));
|
||||
return RegisterEventPromise(PID, RefreshSession);
|
||||
}
|
||||
|
||||
public async virtual Task<Promise> CheckIdentity(RSA check, ushort nonce)
|
||||
{
|
||||
long pID;
|
||||
Task connect = Connect();
|
||||
Task connect = StatusCheck();
|
||||
string ser;
|
||||
using(BitWriter writer = new BitWriter())
|
||||
{
|
||||
@ -173,25 +242,51 @@ namespace Client
|
||||
HasValue = true,
|
||||
Value = "ERROR"
|
||||
};
|
||||
await Connect();
|
||||
client.Send(CreateCommandMessage("Reg", username.ToBase64String() + ":" + password.ToBase64String(), out long pID));
|
||||
return RegisterPromise(pID);
|
||||
await StatusCheck();
|
||||
client.Send(CreateCommandMessage("Reg", DataSet(username, password), out long pID));
|
||||
return RegisterEventPromise(pID, p =>
|
||||
{
|
||||
bool b = !p.Value.StartsWith("ERROR");
|
||||
if (b) // Set proper state before notifying listener
|
||||
{
|
||||
loginTimeout = 280 * TimeSpan.TicksPerSecond;
|
||||
sessionID = p.Value;
|
||||
}
|
||||
PostPromise(p.handler, b);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public async virtual Task Logout(string sessionID)
|
||||
public async virtual Task Logout()
|
||||
{
|
||||
if (!IsLoggedIn) return; // No need to unnecessarily trigger a logout that we know will fail
|
||||
await Connect();
|
||||
await StatusCheck(true);
|
||||
client.Send(CreateCommandMessage("Logout", sessionID, out long _));
|
||||
}
|
||||
|
||||
public async virtual Task<Promise> Refresh()
|
||||
{
|
||||
await StatusCheck(true);
|
||||
client.Send(CreateCommandMessage("Refresh", sessionID, out long pid));
|
||||
return RegisterPromise(pid);
|
||||
}
|
||||
|
||||
protected Promise RegisterPromise(long pID)
|
||||
{
|
||||
Promise p = new Promise();
|
||||
promises[pID] = p;
|
||||
promises[pID] = new Tuple<Promise, Common.Proxy<bool>>(p, false);
|
||||
return p;
|
||||
}
|
||||
|
||||
public void CancelPromise(Promise p)
|
||||
{
|
||||
foreach(var entry in promises)
|
||||
if (entry.Value.Item1.Equals(p))
|
||||
{
|
||||
entry.Value.Item2.Value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected Promise RegisterEventPromise(long pID, Func<Promise, bool> a)
|
||||
{
|
||||
Promise p = RegisterPromise(pID);
|
||||
@ -206,7 +301,7 @@ namespace Client
|
||||
|
||||
protected bool RefreshSession(Promise p)
|
||||
{
|
||||
if (!p.Value.StartsWith("ERROR")) loginTimeout = 280 * TimeSpan.TicksPerSecond;
|
||||
if (!p.Value.StartsWith("ERROR")) RefreshTimeout();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -226,6 +321,45 @@ namespace Client
|
||||
return l;
|
||||
}
|
||||
|
||||
protected void SetAutoRefresh(bool doAR)
|
||||
{
|
||||
if (RefreshSessions == doAR) return;
|
||||
if (RefreshSessions = doAR)
|
||||
{
|
||||
sessionChecker = new Task(DoRefresh);
|
||||
sessionChecker.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void DoRefresh()
|
||||
{
|
||||
// Refresher calls refresh 1500ms before expiry (or asap if less time is available)
|
||||
Task.Delay((int)((Math.Min(0, loginTimeout - DateTime.Now.Ticks - 1500)) / TimeSpan.TicksPerMillisecond));
|
||||
Task<Promise> t = null;
|
||||
if (IsLoggedIn)
|
||||
{
|
||||
t = Refresh();
|
||||
if (RefreshSessions)
|
||||
{
|
||||
sessionChecker = new Task(DoRefresh);
|
||||
sessionChecker.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void RefreshTimeout() => loginTimeout = 280 * TimeSpan.TicksPerSecond + DateTime.Now.Ticks;
|
||||
protected string CreateCommandMessage(string command, string message, out long promiseID) => command + ":" + (promiseID = GetNewPromiseUID()) + ":" + message;
|
||||
protected static string DataSet(params dynamic[] data)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (var datum in data)
|
||||
if(datum!=null)
|
||||
builder.Append(datum.ToString().ToBase64String()).Append(':');
|
||||
|
||||
if (builder.Length != 0) --builder.Length;
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
protected static void PostPromise(Promise p, dynamic value)
|
||||
{
|
||||
p.Value = value?.ToString() ?? "null";
|
||||
@ -238,33 +372,20 @@ namespace Client
|
||||
error = !long.TryParse(response.Substring(0, Math.Max(0, response.IndexOf(':'))), out promiseID);
|
||||
return response.Substring(Math.Max(0, response.IndexOf(':') + 1));
|
||||
}
|
||||
protected string CreateCommandMessage(string command, string message, out long promiseID) => command + ":" + (promiseID = GetNewPromiseUID()) + ":" + message;
|
||||
|
||||
protected static void AwaitTask(Task t)
|
||||
{
|
||||
if (IsTaskAlive(t)) t.Wait();
|
||||
}
|
||||
|
||||
public delegate void Event(Promise p);
|
||||
public class Promise
|
||||
protected static bool IsTaskAlive(Task t) => t != null && !t.IsCompleted && ((t.Status & TaskStatus.Created) == 0);
|
||||
public static void Subscribe(Task<Promise> t, Event e)
|
||||
{
|
||||
internal Promise handler = null; // For chained promise management
|
||||
private Event evt;
|
||||
public string Value { get; internal set; }
|
||||
public bool HasValue { get; internal set; }
|
||||
public Event Subscribe
|
||||
new Task(() =>
|
||||
{
|
||||
get => evt;
|
||||
set
|
||||
{
|
||||
// Allows clearing subscriptions
|
||||
if (evt == null || value == null) evt = value;
|
||||
else evt += value;
|
||||
if (HasValue)
|
||||
evt(this);
|
||||
}
|
||||
}
|
||||
public static Promise AwaitPromise(Task<Promise> p)
|
||||
{
|
||||
//if (!p.IsCompleted) p.RunSynchronously();
|
||||
p.Wait();
|
||||
return p.Result;
|
||||
Promise.AwaitPromise(t);
|
||||
t.Result.Subscribe = e;
|
||||
}).Start();
|
||||
}
|
||||
}
|
||||
}
|
@ -67,8 +67,9 @@
|
||||
<Compile Include="ConsoleForms\Timer.cs" />
|
||||
<Compile Include="ConsoleForms\ViewData.cs" />
|
||||
<Compile Include="Context\NetContext.cs" />
|
||||
<Compile Include="Networking.cs" />
|
||||
<Compile Include="BankNetInteractor.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Promise.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Properties\Resources.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
|
@ -64,5 +64,6 @@ namespace Client.ConsoleForms
|
||||
foreach (var viewEntry in views)
|
||||
Hide(viewEntry.Item2);
|
||||
}
|
||||
public string GetIntlString(string i18n) => manager.GetIntlString(i18n);
|
||||
}
|
||||
}
|
||||
|
@ -26,5 +26,7 @@ namespace Client.ConsoleForms
|
||||
|
||||
public bool Update(ConsoleController.KeyEvent keypress, bool hasKeypress = true)
|
||||
=> Current?.Update(keypress, hasKeypress) == true;
|
||||
|
||||
public string GetIntlString(string i18n) => I18n.MapIfExists(i18n);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,9 @@ namespace Client.ConsoleForms.Graphics
|
||||
|
||||
public override bool HandleKeyEvent(ConsoleController.KeyEvent info, bool inFocus)
|
||||
{
|
||||
return base.HandleKeyEvent(info, inFocus);
|
||||
bool b = inFocus && info.ValidEvent && info.Event.Key == ConsoleKey.Enter;
|
||||
if (b) evt?.Invoke(this);
|
||||
return b;
|
||||
}
|
||||
|
||||
public void SetEvent(SubmissionEvent listener) => evt = listener;
|
||||
|
@ -12,6 +12,8 @@ namespace Client.ConsoleForms.Graphics
|
||||
public int ViewCount { get => innerViews.Count; }
|
||||
public ConsoleColor SelectBackground { get; set; }
|
||||
public ConsoleColor SelectText { get; set; }
|
||||
private int maxWidth;
|
||||
private readonly bool limited;
|
||||
|
||||
|
||||
public override Region Occlusion => new Region(new Rectangle(-padding.Left(), -padding.Top(), ContentWidth + padding.Right(), ContentHeight + padding.Bottom()));
|
||||
@ -22,17 +24,55 @@ namespace Client.ConsoleForms.Graphics
|
||||
SelectText = (ConsoleColor)parameters.AttribueAsInt("text_select_color", (int)ConsoleColor.Gray);
|
||||
|
||||
|
||||
int maxWidth = parameters.AttribueAsInt("width", -1);
|
||||
bool limited = maxWidth != -1;
|
||||
maxWidth = parameters.AttribueAsInt("width", -1);
|
||||
limited = maxWidth != -1;
|
||||
|
||||
foreach (var view in parameters.nestedData.FirstOrNull(n => n.Name.Equals("Views"))?.nestedData ?? new List<ViewData>())
|
||||
{
|
||||
// Limit content width
|
||||
if (limited && view.AttribueAsInt("width") > maxWidth) view.attributes["width"] = maxWidth.ToString();
|
||||
|
||||
Tuple<string, View> v = ConsoleController.LoadView(parameters.attributes["xmlns"], view, I18n); // Load the view in with standard namespace
|
||||
innerViews.Add(ConsoleController.LoadView(parameters.attributes["xmlns"], view, I18n)); // Load the view in with standard namespace
|
||||
}
|
||||
|
||||
ComputeSize();
|
||||
|
||||
SelectedView = 0;
|
||||
}
|
||||
|
||||
|
||||
// Optimized to add multiple view before recomputing size
|
||||
public void AddViews(params Tuple<string, View>[] data)
|
||||
{
|
||||
foreach (var datum in data)
|
||||
{
|
||||
datum.Item2.DrawBorder = false;
|
||||
_AddView(datum.Item2, datum.Item1);
|
||||
}
|
||||
ComputeSize();
|
||||
}
|
||||
// Add single view
|
||||
public void AddView(View v, string viewID)
|
||||
{
|
||||
_AddView(v, viewID);
|
||||
ComputeSize();
|
||||
}
|
||||
// Add view without recomputing layout size
|
||||
private void _AddView(View v, string viewID)
|
||||
{
|
||||
foreach (var data in innerViews)
|
||||
if (data.Item1 != null && data.Item1.Equals(viewID))
|
||||
throw new SystemException("Cannot load view with same id"); // TODO: Replace with custom exception
|
||||
innerViews.Add(new Tuple<string, View>(viewID, v));
|
||||
}
|
||||
|
||||
protected void ComputeSize()
|
||||
{
|
||||
ContentHeight = 0;
|
||||
foreach(var v in innerViews)
|
||||
{
|
||||
v.Item2.DrawBorder = false;
|
||||
innerViews.Add(v);
|
||||
//innerViews.Add(v);
|
||||
|
||||
if (!limited) maxWidth = Math.Max(v.Item2.ContentWidth, maxWidth);
|
||||
|
||||
@ -40,12 +80,11 @@ namespace Client.ConsoleForms.Graphics
|
||||
}
|
||||
++ContentHeight;
|
||||
|
||||
SelectedView = 0;
|
||||
|
||||
ContentWidth = maxWidth;
|
||||
}
|
||||
|
||||
public View GetView(string name) => innerViews.FirstOrNull(v => v.Item1.Equals(name))?.Item2;
|
||||
public T GetView<T>(string name) where T : View => (T)GetView(name);
|
||||
|
||||
protected override void _Draw(int left, ref int top)
|
||||
{
|
||||
|
@ -72,6 +72,8 @@ namespace Client.ConsoleForms.Parameters
|
||||
return this;
|
||||
}
|
||||
|
||||
public ViewData AddNestedSimple(string name, string text = "") => AddNested(new ViewData(name, text));
|
||||
|
||||
public string GetAttribute(string attr, string def = "") => attributes.ContainsKey(attr) ? attributes[attr] : def;
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,8 @@ namespace Client
|
||||
|
||||
bool connecting = false;
|
||||
|
||||
|
||||
|
||||
GetView<InputView>("NetConnect").SubmissionsListener = i =>
|
||||
{
|
||||
if (connecting)
|
||||
@ -38,25 +40,6 @@ namespace Client
|
||||
connecting = true;
|
||||
// Connect to server here
|
||||
BankNetInteractor ita = new BankNetInteractor(i.Inputs[0].Text, prt);
|
||||
/*
|
||||
try
|
||||
{
|
||||
//var t = ita.Connect();
|
||||
//while (!t.IsCompleted)
|
||||
// if (t.IsCanceled || t.IsFaulted)
|
||||
// {
|
||||
// Show("ConnectError");
|
||||
// return;
|
||||
// }
|
||||
// else System.Threading.Thread.Sleep(125);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Show("ConnectionError");
|
||||
connecting = false;
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
Promise verify;
|
||||
try
|
||||
@ -66,6 +49,7 @@ namespace Client
|
||||
catch
|
||||
{
|
||||
Show("ConnectionError");
|
||||
connecting = false;
|
||||
return;
|
||||
}
|
||||
verify.Subscribe =
|
||||
@ -74,12 +58,13 @@ namespace Client
|
||||
void load() => manager.LoadContext(new WelcomeContext(manager, ita));
|
||||
|
||||
// Add condition check for remote peer verification
|
||||
if (bool.Parse(p.Value)) controller.Popup("Server identity verified!", 1000, ConsoleColor.Green, load);
|
||||
else controller.Popup("Remote server identity could not be verified!", 5000, ConsoleColor.Red, load);
|
||||
if (bool.Parse(p.Value)) controller.Popup(GetIntlString("@string/NC_verified"), 1000, ConsoleColor.Green, load);
|
||||
else controller.Popup(GetIntlString("@string/verror"), 5000, ConsoleColor.Red, load);
|
||||
};
|
||||
DialogView identityNotify = GetView<DialogView>("IdentityVerify");
|
||||
identityNotify.RegisterSelectListener(
|
||||
(vw, ix, nm) => {
|
||||
connecting = false;
|
||||
verify.Subscribe = null; // Clear subscription
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
ita.CancelAll();
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Client.ConsoleForms;
|
||||
using Client.ConsoleForms.Graphics;
|
||||
using Client.ConsoleForms.Parameters;
|
||||
using Client.Properties;
|
||||
using ConsoleForms;
|
||||
using System;
|
||||
@ -16,25 +17,107 @@ namespace Client
|
||||
public sealed class SessionContext : Context
|
||||
{
|
||||
private readonly BankNetInteractor interactor;
|
||||
private readonly string sessionID;
|
||||
private bool scheduleDestroy;
|
||||
private Promise userDataGetter;
|
||||
private Promise accountsGetter;
|
||||
private List<string> accounts = null;
|
||||
private string username;
|
||||
private bool isAdministrator = false;
|
||||
|
||||
public SessionContext(ContextManager manager, BankNetInteractor interactor, string sessionID) : base(manager, "Session", "Common")
|
||||
|
||||
public SessionContext(ContextManager manager, BankNetInteractor interactor) : base(manager, "Session", "Common")
|
||||
{
|
||||
this.interactor = interactor;
|
||||
this.sessionID = sessionID;
|
||||
scheduleDestroy = !interactor.IsLoggedIn;
|
||||
|
||||
GetView<DialogView>("Success").RegisterSelectListener((v, i, s) =>
|
||||
{
|
||||
interactor.Logout(sessionID);
|
||||
interactor.Logout();
|
||||
manager.LoadContext(new NetContext(manager));
|
||||
});
|
||||
|
||||
// Menu option setup
|
||||
ListView options = GetView<ListView>("menu_options");
|
||||
options.GetView<ButtonView>("exit").SetEvent(v =>
|
||||
{
|
||||
interactor.Logout();
|
||||
manager.LoadContext(new NetContext(manager));
|
||||
});
|
||||
|
||||
options.GetView<ButtonView>("view").SetEvent(v =>
|
||||
{
|
||||
if (!accountsGetter.HasValue) Show("data_fetch");
|
||||
accountsGetter.Subscribe = p =>
|
||||
{
|
||||
Hide("data_fetch");
|
||||
|
||||
void SubmitListener(View listener)
|
||||
{
|
||||
ButtonView view = listener as ButtonView;
|
||||
|
||||
}
|
||||
|
||||
public override void OnCreate() => Show("menu_options");
|
||||
var list = GetView<ListView>("account_show");
|
||||
var data = p.Value.Split('&');
|
||||
bool b = data.Length == 1 && data[0].Length == 0;
|
||||
Tuple<string, View>[] listData = new Tuple<string, View>[data.Length - (b?1:0)];
|
||||
if(!b)
|
||||
for(int i = 0; i<listData.Length; ++i)
|
||||
{
|
||||
ButtonView t = new ButtonView(new ViewData("ButtonView").AddNestedSimple("Text", data[i]), LangManager.NO_LANG); // Don't do translations
|
||||
t.SetEvent(SubmitListener);
|
||||
listData[i] = new Tuple<string, View>(data[i].FromBase64String(), t);
|
||||
}
|
||||
string dismiss = GetIntlString("@string/GENERIC_dismiss");
|
||||
ButtonView exit = list.GetView<ButtonView>("close");
|
||||
exit.SetEvent(_ => Hide(list));
|
||||
list.AddViews(listData);
|
||||
Show(list);
|
||||
};
|
||||
});
|
||||
|
||||
// Update password
|
||||
options.GetView<ButtonView>("password_update").SetEvent(v =>
|
||||
{
|
||||
|
||||
});
|
||||
|
||||
if (!scheduleDestroy)
|
||||
{
|
||||
// We have a valid context!
|
||||
userDataGetter = Promise.AwaitPromise(interactor.UserInfo()); // Get basic user info
|
||||
accountsGetter = Promise.AwaitPromise(interactor.ListUserAccounts()); // Get accounts associated with this user
|
||||
|
||||
userDataGetter.Subscribe = p =>
|
||||
{
|
||||
var data = p.Value.Split('&');
|
||||
username = data[0].FromBase64String();
|
||||
isAdministrator = bool.Parse(data[1]);
|
||||
};
|
||||
|
||||
accountsGetter.Subscribe = p =>
|
||||
{
|
||||
var data = p.Value.Split('&');
|
||||
accounts = new List<string>();
|
||||
accounts.AddRange(data);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleLogout()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override void OnCreate()
|
||||
{
|
||||
if (scheduleDestroy) manager.LoadContext(new WelcomeContext(manager, interactor));
|
||||
else Show("menu_options");
|
||||
}
|
||||
|
||||
public override void OnDestroy()
|
||||
{
|
||||
controller.CloseView(views.GetNamed("Success"));
|
||||
base.HideAll();
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
interactor.CancelAll();
|
||||
#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
|
@ -13,7 +13,6 @@ namespace Client
|
||||
public sealed class WelcomeContext : Context
|
||||
{
|
||||
private readonly BankNetInteractor interactor;
|
||||
private long token;
|
||||
private Promise promise;
|
||||
private bool forceDestroy = true;
|
||||
|
||||
@ -64,7 +63,7 @@ namespace Client
|
||||
else
|
||||
{
|
||||
forceDestroy = false;
|
||||
manager.LoadContext(new SessionContext(manager, interactor, response.Value));
|
||||
manager.LoadContext(new SessionContext(manager, interactor));
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -113,12 +112,12 @@ namespace Client
|
||||
response =>
|
||||
{
|
||||
Hide("RegWait");
|
||||
if (response.Value.StartsWith("ERROR"))
|
||||
if (!bool.Parse(response.Value))
|
||||
Show("DuplicateAccountError");
|
||||
else
|
||||
{
|
||||
forceDestroy = false;
|
||||
manager.LoadContext(new SessionContext(manager, interactor, response.Value));
|
||||
manager.LoadContext(new SessionContext(manager, interactor));
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -184,7 +183,7 @@ namespace Client
|
||||
if (promise != null && !promise.HasValue) promise.Subscribe = null;
|
||||
|
||||
// Stop listening
|
||||
interactor.UnregisterListener(token);
|
||||
//interactor.UnregisterListener(token);
|
||||
|
||||
#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
|
||||
if (forceDestroy) interactor.CancelAll();
|
||||
|
@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
|
||||
using Client;
|
||||
using Client.ConsoleForms;
|
||||
using Client.ConsoleForms.Parameters;
|
||||
using Common;
|
||||
using Tofvesson.Common;
|
||||
using Tofvesson.Crypto;
|
||||
|
||||
namespace ConsoleForms
|
||||
|
35
Client/Promise.cs
Normal file
35
Client/Promise.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Client
|
||||
{
|
||||
public delegate void Event(Promise p);
|
||||
public class Promise
|
||||
{
|
||||
internal Promise handler = null; // For chained promise management
|
||||
private Event evt;
|
||||
public string Value { get; internal set; }
|
||||
public bool HasValue { get; internal set; }
|
||||
public Event Subscribe
|
||||
{
|
||||
get => evt;
|
||||
set
|
||||
{
|
||||
// Allows clearing subscriptions
|
||||
if (evt == null || value == null) evt = value;
|
||||
else evt += value;
|
||||
if (HasValue)
|
||||
evt(this);
|
||||
}
|
||||
}
|
||||
public static Promise AwaitPromise(Task<Promise> p)
|
||||
{
|
||||
//if (!p.IsCompleted) p.RunSynchronously();
|
||||
p.Wait();
|
||||
return p.Result;
|
||||
}
|
||||
}
|
||||
}
|
@ -22,4 +22,12 @@
|
||||
</Options>
|
||||
<Text>@string/NC_connerr</Text>
|
||||
</DialogView>
|
||||
|
||||
<TextView id="data_fetch"
|
||||
padding_left="2"
|
||||
padding_right="2"
|
||||
padding_top="1"
|
||||
padding_bottom="1">
|
||||
<Text>@string/GENERiC_fetch</Text>
|
||||
</TextView>
|
||||
</Resources>
|
@ -19,12 +19,31 @@
|
||||
<Text>@string/SE_bal</Text>
|
||||
</TextView>
|
||||
|
||||
<!-- Password update prompt -->
|
||||
<InputView id="password_update"
|
||||
padding_left="2"
|
||||
padding_right="2"
|
||||
padding_top="1"
|
||||
padding_bottom="1">
|
||||
<Fields>
|
||||
<Field hide="true">@string/SU_pwd</Field>
|
||||
<Field hide="true">@string/SU_pwdrep</Field>
|
||||
</Fields>
|
||||
|
||||
<Text>@string/SE_pwdu</Text>
|
||||
</InputView>
|
||||
|
||||
<!-- Session account actions -->
|
||||
<ListView id="menu_options"
|
||||
padding_left="2"
|
||||
padding_right="2"
|
||||
padding_top="1"
|
||||
padding_bottom="1">
|
||||
<Views>
|
||||
<ButtonView id="add">
|
||||
<Text>@string/SE_open</Text>
|
||||
</ButtonView>
|
||||
|
||||
<ButtonView id="view">
|
||||
<Text>@string/SE_view</Text>
|
||||
</ButtonView>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Strings label="English">
|
||||
<Entry name="NC_head">Server configuration</Entry>
|
||||
<Entry name="NC_sec">The selected server's identity could not be verified. This implies that it is not an official server. Continue?</Entry>
|
||||
@ -11,8 +11,12 @@
|
||||
<Entry name="NC_porterr">The supplied port is not valid</Entry>
|
||||
<Entry name="NC_connerr">Could not connect to server</Entry>
|
||||
<Entry name="NC_identity">Verifying server identity...</Entry>
|
||||
<Entry name="NC_verified">Server identity verified!</Entry>
|
||||
<Entry name="NC_verror">Remote server identity could not be verified!</Entry>
|
||||
|
||||
<Entry name="SU_welcome">Welcome to the Tofvesson banking system! To continue, press [ENTER] To go back, press [ESCAPE]</Entry>
|
||||
<Entry name="SU_welcome">Welcome to the Tofvesson banking system!
|
||||
To continue, press [ENTER]
|
||||
To go back, press [ESCAPE]</Entry>
|
||||
<Entry name="SU_reg">Register Account</Entry>
|
||||
<Entry name="SU_regstall">Registering...</Entry>
|
||||
<Entry name="SU_dup">An account with this username already exists!</Entry>
|
||||
@ -43,7 +47,10 @@
|
||||
<Entry name="SE_info">$0
|
||||
Balance: $1
|
||||
Date of creation: $2</Entry>
|
||||
<Entry name="SE_autolo">You were automatically logged out due to inactivity</Entry>
|
||||
<Entry name="SE_lo">Logged out</Entry>
|
||||
|
||||
<Entry name="GENERIC_fetch">Fetching data...</Entry>
|
||||
<Entry name="GENERIC_dismiss">Close</Entry>
|
||||
<Entry name="GENERIC_accept">Ok</Entry>
|
||||
<Entry name="GENERIC_positive">Yes</Entry>
|
||||
|
@ -11,8 +11,12 @@
|
||||
<Entry name="NC_porterr">The supplied port is not valid</Entry>
|
||||
<Entry name="NC_connerr">Could not connect to server</Entry>
|
||||
<Entry name="NC_identity">Verifying server identity...</Entry>
|
||||
<Entry name="NC_verified">Server identity verified!</Entry>
|
||||
<Entry name="NC_verror">Remote server identity could not be verified!</Entry>
|
||||
|
||||
<Entry name="SU_welcome">Welcome to the Tofvesson banking system! To continue, press [ENTER] To go back, press [ESCAPE]</Entry>
|
||||
<Entry name="SU_welcome">Welcome to the Tofvesson banking system!
|
||||
To continue, press [ENTER]
|
||||
To go back, press [ESCAPE]</Entry>
|
||||
<Entry name="SU_reg">Register Account</Entry>
|
||||
<Entry name="SU_regstall">Registering...</Entry>
|
||||
<Entry name="SU_dup">An account with this username already exists!</Entry>
|
||||
@ -43,7 +47,10 @@
|
||||
<Entry name="SE_info">$0
|
||||
Balance: $1
|
||||
Date of creation: $2</Entry>
|
||||
<Entry name="SE_autolo">You were automatically logged out due to inactivity</Entry>
|
||||
<Entry name="SE_lo">Logged out</Entry>
|
||||
|
||||
<Entry name="GENERIC_fetch">Fetching data...</Entry>
|
||||
<Entry name="GENERIC_dismiss">Close</Entry>
|
||||
<Entry name="GENERIC_accept">Ok</Entry>
|
||||
<Entry name="GENERIC_positive">Yes</Entry>
|
||||
|
@ -11,6 +11,8 @@
|
||||
<Entry name="NC_porterr">Den givna porten är inte giltig</Entry>
|
||||
<Entry name="NC_connerr">Kunde inte koppla till servern</Entry>
|
||||
<Entry name="NC_identity">Verifierar serverns identitet...</Entry>
|
||||
<Entry name="NC_verified">Serveridentitet verifierad!</Entry>
|
||||
<Entry name="NC_verror">Serveridentitet kunde inte verifieras!</Entry>
|
||||
|
||||
<Entry name="SU_welcome">Välkommen till Tofvessons banksystem!
|
||||
För att fortsätta, tryck [ENTER]
|
||||
@ -45,7 +47,10 @@
|
||||
<Entry name="SE_info">"$0"
|
||||
Kontobalans: $1
|
||||
Begynnelsedatum: $2</Entry>
|
||||
<Entry name="SE_autolo">Du har automatiskt loggats ut p.g.a. inaktivitet</Entry>
|
||||
<Entry name="SE_lo">Utloggad</Entry>
|
||||
|
||||
<Entry name="GENERIC_fetch">Hämtar data...</Entry>
|
||||
<Entry name="GENERIC_dismiss">Stäng</Entry>
|
||||
<Entry name="GENERIC_accept">Ok</Entry>
|
||||
<Entry name="GENERIC_positive">Ja</Entry>
|
||||
|
@ -6,7 +6,7 @@ using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Tofvesson.Crypto;
|
||||
|
||||
namespace Common
|
||||
namespace Tofvesson.Common
|
||||
{
|
||||
public class User
|
||||
{
|
||||
|
@ -88,7 +88,7 @@ namespace Tofvesson.Common
|
||||
Debug.WriteLine("BitWriter: The type \"" + b.GetType() + "\" is not supported by the Binary Serializer. It will be ignored!");
|
||||
}
|
||||
|
||||
|
||||
// Public write methods to prevent errors when chaning types
|
||||
public void WriteBool(bool b) => Push(b);
|
||||
public void WriteFloat(float f) => Push(f);
|
||||
public void WriteDouble(double d) => Push(d);
|
||||
@ -126,6 +126,7 @@ namespace Tofvesson.Common
|
||||
foreach (T t1 in t) Push(signed ? (object)ZigZagEncode(t1 as long? ?? t1 as int? ?? t1 as short? ?? t1 as sbyte? ?? 0, size) : (object)t1);
|
||||
}
|
||||
|
||||
// Actually serialize
|
||||
public byte[] Finalize()
|
||||
{
|
||||
byte[] b = new byte[GetFinalizeSize()];
|
||||
@ -157,9 +158,11 @@ namespace Tofvesson.Common
|
||||
}
|
||||
else Serialize(item, buffer, ref bitOffset, ref isAligned);
|
||||
|
||||
collect.Clear(); // Allow GC to clean up any dangling references
|
||||
return (bitCount / 8) + (bitCount % 8 == 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
// Compute output size (in bytes)
|
||||
public long GetFinalizeSize()
|
||||
{
|
||||
long bitCount = 0;
|
||||
@ -167,6 +170,8 @@ namespace Tofvesson.Common
|
||||
return ((bitCount / 8) + (bitCount % 8 == 0 ? 0 : 1));
|
||||
}
|
||||
|
||||
// Serialization: Originally used dynamic, but due to the requirement of an adaptation using without dynamic,
|
||||
// it has been completely retrofitted to not use dynamic
|
||||
private static void Serialize<T>(T t, byte[] writeTo, ref long bitOffset, ref bool isAligned)
|
||||
{
|
||||
Type type = t.GetType();
|
||||
@ -238,6 +243,7 @@ namespace Tofvesson.Common
|
||||
else if (t is uint) value = t as uint? ?? 0;
|
||||
else /*if (t is ulong)*/ value = t as ulong? ?? 0;
|
||||
|
||||
// VarInt implementation
|
||||
if (value <= 240) WriteByte(writeTo, (byte)value, bitOffset, isAligned);
|
||||
else if (value <= 2287)
|
||||
{
|
||||
@ -287,9 +293,11 @@ namespace Tofvesson.Common
|
||||
}
|
||||
}
|
||||
|
||||
// Write oddly bounded data
|
||||
private static byte Read7BitRange(byte higher, byte lower, int bottomBits) => (byte)((higher << bottomBits) & (lower & (0xFF << (8-bottomBits))));
|
||||
private static byte ReadNBits(byte from, int offset, int count) => (byte)(from & ((0xFF >> (8-count)) << offset));
|
||||
|
||||
// Check if type is signed (int, long, etc)
|
||||
private static bool IsSigned(Type t) => t == typeof(sbyte) || t == typeof(short) || t == typeof(int) || t == typeof(long);
|
||||
|
||||
private static Type GetUnsignedType(Type t) =>
|
||||
@ -299,8 +307,10 @@ namespace Tofvesson.Common
|
||||
t == typeof(long) ? typeof(ulong) :
|
||||
null;
|
||||
|
||||
// Encode signed values in a way that preserves magnitude
|
||||
private static ulong ZigZagEncode(long d, int bytes) => (ulong)(((d >> (bytes * 8 - 1))&1) | (d << 1));
|
||||
|
||||
// Gets the amount of bits required to serialize a given value
|
||||
private static long GetBitCount<T>(T t)
|
||||
{
|
||||
Type type = t.GetType();
|
||||
@ -328,6 +338,7 @@ namespace Tofvesson.Common
|
||||
return count;
|
||||
}
|
||||
|
||||
// Write methods
|
||||
private static void WriteBit(byte[] b, bool bit, long index)
|
||||
=> b[index / 8] = (byte)((b[index / 8] & ~(1 << (int)(index % 8))) | (bit ? 1 << (int)(index % 8) : 0));
|
||||
private static void WriteByte(byte[] b, ulong value, long index, bool isAligned) => WriteByte(b, (byte)value, index, isAligned);
|
||||
|
@ -81,6 +81,7 @@
|
||||
<Compile Include="Padding.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Cryptography\RSA.cs" />
|
||||
<Compile Include="Proxy.cs" />
|
||||
<Compile Include="SHA.cs" />
|
||||
<Compile Include="Streams.cs" />
|
||||
<Compile Include="Support.cs" />
|
||||
|
@ -8,7 +8,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Tofvesson.Crypto;
|
||||
|
||||
namespace Common.Cryptography
|
||||
namespace Tofvesson.Common.Cryptography
|
||||
{
|
||||
public class EllipticCurve
|
||||
{
|
||||
|
@ -6,7 +6,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Tofvesson.Crypto;
|
||||
|
||||
namespace Common.Cryptography.KeyExchange
|
||||
namespace Tofvesson.Common.Cryptography.KeyExchange
|
||||
{
|
||||
public sealed class DiffieHellman : IKeyExchange
|
||||
{
|
||||
|
@ -7,7 +7,7 @@ using System.Threading.Tasks;
|
||||
using Tofvesson.Common;
|
||||
using Tofvesson.Crypto;
|
||||
|
||||
namespace Common.Cryptography.KeyExchange
|
||||
namespace Tofvesson.Common.Cryptography.KeyExchange
|
||||
{
|
||||
public class EllipticDiffieHellman : IKeyExchange
|
||||
{
|
||||
|
@ -5,7 +5,7 @@ using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Common.Cryptography.KeyExchange
|
||||
namespace Tofvesson.Common.Cryptography.KeyExchange
|
||||
{
|
||||
public interface IKeyExchange
|
||||
{
|
||||
|
@ -5,7 +5,7 @@ using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Common.Cryptography
|
||||
namespace Tofvesson.Common.Cryptography
|
||||
{
|
||||
public class Point
|
||||
{
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Common.Cryptography.KeyExchange;
|
||||
using Tofvesson.Common.Cryptography.KeyExchange;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@ -12,7 +12,7 @@ using System.Threading.Tasks;
|
||||
using Tofvesson.Common;
|
||||
using Tofvesson.Crypto;
|
||||
|
||||
namespace Common
|
||||
namespace Tofvesson.Net
|
||||
{
|
||||
public delegate string OnMessageRecieved(string request, Dictionary<string, string> associations, ref bool stayAlive);
|
||||
public delegate void OnClientConnectStateChanged(NetClient client, bool connect);
|
||||
@ -31,14 +31,18 @@ namespace Common
|
||||
private Thread eventListener;
|
||||
|
||||
// Communication parameters
|
||||
protected readonly Queue<byte[]> messageBuffer = new Queue<byte[]>();
|
||||
public readonly Dictionary<string, string> assignedValues = new Dictionary<string, string>();
|
||||
protected readonly Queue<byte[]> messageBuffer = new Queue<byte[]>(); // Outbound communication buffer
|
||||
public readonly Dictionary<string, string> assignedValues = new Dictionary<string, string>(); // Local connection-related "variables"
|
||||
protected readonly OnMessageRecieved handler;
|
||||
protected internal readonly OnClientConnectStateChanged onConn;
|
||||
protected readonly IPAddress target;
|
||||
protected readonly int bufSize;
|
||||
protected readonly IKeyExchange exchange;
|
||||
protected internal long lastComm = DateTime.Now.Ticks; // Latest comunication event (in ticks)
|
||||
protected readonly IPAddress target; // Remote target IP-address
|
||||
protected readonly int bufSize; // Communication buffer size
|
||||
protected readonly IKeyExchange exchange; // Cryptographic key exchange algorithm
|
||||
protected internal long lastComm = DateTime.Now.Ticks;
|
||||
public IPEndPoint Remote
|
||||
{
|
||||
get => (IPEndPoint) Connection?.RemoteEndPoint;
|
||||
}
|
||||
|
||||
// Connection to peer
|
||||
protected Socket Connection { get; private set; }
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Common.Cryptography.KeyExchange;
|
||||
using Tofvesson.Common.Cryptography.KeyExchange;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@ -10,7 +10,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tofvesson.Crypto;
|
||||
|
||||
namespace Common
|
||||
namespace Tofvesson.Net
|
||||
{
|
||||
public sealed class NetServer
|
||||
{
|
||||
|
@ -8,7 +8,7 @@ using System.Threading.Tasks;
|
||||
using Tofvesson.Common;
|
||||
using Tofvesson.Crypto;
|
||||
|
||||
namespace Common
|
||||
namespace Tofvesson.Net
|
||||
{
|
||||
// Helper methods. WithHeader() should really just be in Support.cs
|
||||
public static class NetSupport
|
||||
|
19
Common/Proxy.cs
Normal file
19
Common/Proxy.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Common
|
||||
{
|
||||
// Enables mutability for immutable values or parameters
|
||||
public sealed class Proxy<T>
|
||||
{
|
||||
public T Value { get; set; }
|
||||
|
||||
public Proxy(T initial = default(T)) => Value = initial;
|
||||
|
||||
public static implicit operator T(Proxy<T> p) => p.Value;
|
||||
public static implicit operator Proxy<T>(T t) => new Proxy<T>(t);
|
||||
}
|
||||
}
|
@ -82,6 +82,8 @@ namespace Tofvesson.Crypto
|
||||
public byte Get(int idx) => (byte)((idx < 4 ? i0 : idx < 8 ? i1 : idx < 12 ? i2 : idx < 16 ? i3 : i4)>>(8*(idx%4)));
|
||||
}
|
||||
private static readonly uint[] block = new uint[80];
|
||||
// Memory-optimized implementation of Secure Hashing Algorithm 1 (SHA1)
|
||||
// NOTE: This method is NOT thread-safe!
|
||||
public static SHA1Result SHA1_Opt(byte[] message)
|
||||
{
|
||||
SHA1Result result = new SHA1Result
|
||||
@ -114,7 +116,7 @@ namespace Tofvesson.Crypto
|
||||
|
||||
int chunks = max / 64;
|
||||
|
||||
// Replaces the recurring allocation of 80 uints
|
||||
// Slow (at least with debugger because it spams stack frames)
|
||||
/*uint ComputeIndex(int block, int idx)
|
||||
{
|
||||
if (idx < 16)
|
||||
@ -159,6 +161,8 @@ namespace Tofvesson.Crypto
|
||||
result.i3 += d;
|
||||
result.i4 += e;
|
||||
}
|
||||
|
||||
// Serialize result
|
||||
result.i0 = Support.SwapEndian(result.i0);
|
||||
result.i1 = Support.SwapEndian(result.i1);
|
||||
result.i2 = Support.SwapEndian(result.i2);
|
||||
|
@ -6,7 +6,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Common
|
||||
namespace Tofvesson.Common
|
||||
{
|
||||
public sealed class TimeStampWriter : TextWriter
|
||||
{
|
||||
|
99
Server/ConsoleReader.cs
Normal file
99
Server/ConsoleReader.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
public class ConsoleReader
|
||||
{
|
||||
public static IEnumerable<string> ReadFromBuffer(short x, short y, short width, short height)
|
||||
{
|
||||
IntPtr buffer = Marshal.AllocHGlobal(width * height * Marshal.SizeOf(typeof(CHAR_INFO)));
|
||||
if (buffer == null)
|
||||
throw new OutOfMemoryException();
|
||||
|
||||
try
|
||||
{
|
||||
COORD coord = new COORD();
|
||||
SMALL_RECT rc = new SMALL_RECT();
|
||||
rc.Left = x;
|
||||
rc.Top = y;
|
||||
rc.Right = (short)(x + width - 1);
|
||||
rc.Bottom = (short)(y + height - 1);
|
||||
|
||||
COORD size = new COORD();
|
||||
size.X = width;
|
||||
size.Y = height;
|
||||
|
||||
const int STD_OUTPUT_HANDLE = -11;
|
||||
if (!ReadConsoleOutput(GetStdHandle(STD_OUTPUT_HANDLE), buffer, size, coord, ref rc))
|
||||
{
|
||||
// 'Not enough storage is available to process this command' may be raised for buffer size > 64K (see ReadConsoleOutput doc.)
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
IntPtr ptr = buffer;
|
||||
for (int h = 0; h < height; h++)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int w = 0; w < width; w++)
|
||||
{
|
||||
CHAR_INFO ci = (CHAR_INFO)Marshal.PtrToStructure(ptr, typeof(CHAR_INFO));
|
||||
char[] chars = Console.OutputEncoding.GetChars(ci.charData);
|
||||
sb.Append(chars[0]);
|
||||
ptr += Marshal.SizeOf(typeof(CHAR_INFO));
|
||||
}
|
||||
yield return sb.ToString();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct CHAR_INFO
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
|
||||
public byte[] charData;
|
||||
public short attributes;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct COORD
|
||||
{
|
||||
public short X;
|
||||
public short Y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct SMALL_RECT
|
||||
{
|
||||
public short Left;
|
||||
public short Top;
|
||||
public short Right;
|
||||
public short Bottom;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct CONSOLE_SCREEN_BUFFER_INFO
|
||||
{
|
||||
public COORD dwSize;
|
||||
public COORD dwCursorPosition;
|
||||
public short wAttributes;
|
||||
public SMALL_RECT srWindow;
|
||||
public COORD dwMaximumWindowSize;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool ReadConsoleOutput(IntPtr hConsoleOutput, IntPtr lpBuffer, COORD dwBufferSize, COORD dwBufferCoord, ref SMALL_RECT lpReadRegion);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetStdHandle(int nStdHandle);
|
||||
}
|
||||
}
|
@ -543,9 +543,12 @@ namespace Server
|
||||
Name = name;
|
||||
IsAdministrator = admin;
|
||||
Salt = Convert.ToBase64String(salt);
|
||||
PasswordHash = generatePass ? Convert.ToBase64String(KDF.PBKDF2(KDF.HMAC_SHA1, Encoding.UTF8.GetBytes(passHash), Encoding.UTF8.GetBytes(Salt), 8192, 320)) : passHash;
|
||||
PasswordHash = generatePass ? ComputePass(passHash) : passHash;
|
||||
}
|
||||
|
||||
public string ComputePass(string pass)
|
||||
=> Convert.ToBase64String(KDF.PBKDF2(KDF.HMAC_SHA1, Encoding.UTF8.GetBytes(pass), Encoding.UTF8.GetBytes(Salt), 8192, 320));
|
||||
|
||||
public bool Authenticate(string password)
|
||||
=> Convert.ToBase64String(KDF.PBKDF2(KDF.HMAC_SHA1, Encoding.UTF8.GetBytes(password), Encoding.UTF8.GetBytes(Salt), 8192, 320)).Equals(PasswordHash);
|
||||
|
||||
|
@ -1,18 +1,27 @@
|
||||
using Common;
|
||||
using Tofvesson.Common;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
public static class Output
|
||||
{
|
||||
// Fancy timestamped output
|
||||
private static readonly object writeLock = new object();
|
||||
private static readonly TextWriter stampedError = new TimeStampWriter(Console.Error, "HH:mm:ss.fff");
|
||||
private static readonly TextWriter stampedOutput = new TimeStampWriter(Console.Out, "HH:mm:ss.fff");
|
||||
private static bool overwrite = false;
|
||||
private static short readStart_x = 0, readStart_y = 0;
|
||||
private static bool reading = false;
|
||||
private static List<char> peek = new List<char>();
|
||||
|
||||
public static bool ReadingLine { get => reading; }
|
||||
public static Action OnNewLine { get; set; }
|
||||
|
||||
|
||||
public static void WriteLine(object message, bool error = false, bool timeStamp = true)
|
||||
{
|
||||
if (error) Error(message, true, timeStamp);
|
||||
@ -44,24 +53,63 @@ namespace Server
|
||||
|
||||
private static void Write(string message, ConsoleColor f, ConsoleColor b, bool newline, TextWriter writer)
|
||||
{
|
||||
lock (writeLock)
|
||||
{
|
||||
string read = null;
|
||||
if (reading && newline) read = PeekLine();
|
||||
if (overwrite) ClearLine();
|
||||
overwrite = false;
|
||||
ConsoleColor f1 = Console.ForegroundColor, b1 = Console.BackgroundColor;
|
||||
Console.ForegroundColor = f;
|
||||
Console.BackgroundColor = b;
|
||||
writer.Write(message);
|
||||
readStart_y = (short)Console.CursorTop;
|
||||
if (newline)
|
||||
{
|
||||
writer.WriteLine();
|
||||
readStart_x = 0;
|
||||
++readStart_y;
|
||||
OnNewLine?.Invoke();
|
||||
readStart_y = (short)Console.CursorTop;
|
||||
readStart_x = (short)Console.CursorLeft;
|
||||
if (reading) Console.Out.Write(read);
|
||||
}
|
||||
else readStart_x = (short)Console.CursorLeft;
|
||||
Console.ForegroundColor = f1;
|
||||
Console.BackgroundColor = b1;
|
||||
}
|
||||
}
|
||||
|
||||
// Read currently entered keyboard input (even if enter hasn't been pressed)
|
||||
public static string PeekLine()
|
||||
{
|
||||
IEnumerator<string> e = ConsoleReader.ReadFromBuffer(readStart_x, readStart_y, (short)((short)Console.BufferWidth - readStart_x), 1).GetEnumerator();
|
||||
e.MoveNext();
|
||||
Console.SetCursorPosition(readStart_x, readStart_y);
|
||||
StringBuilder builder = new StringBuilder(e.Current);
|
||||
while (builder.Length > 0 && builder[builder.Length - 1] == ' ') --builder.Length;
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
string peek = PeekLine();
|
||||
Console.Clear();
|
||||
readStart_x = 0;
|
||||
readStart_y = 0;
|
||||
OnNewLine?.Invoke();
|
||||
Console.Out.Write(peek);
|
||||
}
|
||||
|
||||
public static string ReadLine()
|
||||
{
|
||||
if (reading) throw new SystemException("Cannot concurrently read line!");
|
||||
reading = true;
|
||||
readStart_x = (short)Console.CursorLeft;
|
||||
readStart_y = (short)Console.CursorTop;
|
||||
string s = Console.ReadLine();
|
||||
|
||||
reading = false;
|
||||
overwrite = false;
|
||||
OnNewLine?.Invoke();
|
||||
return s;
|
||||
|
@ -1,5 +1,5 @@
|
||||
using Common;
|
||||
using Common.Cryptography.KeyExchange;
|
||||
using Tofvesson.Common;
|
||||
using Tofvesson.Common.Cryptography.KeyExchange;
|
||||
using Server.Properties;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@ -8,14 +8,19 @@ using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Tofvesson.Common;
|
||||
using Tofvesson.Net;
|
||||
using Tofvesson.Crypto;
|
||||
|
||||
namespace Server
|
||||
{
|
||||
class Program
|
||||
{
|
||||
private const string CONSOLE_MOTD = @"Tofvesson Enterprises Banking Server
|
||||
By Gabriel Tofvesson
|
||||
|
||||
Use command 'help' to get a list of available commands";
|
||||
private const string VERBOSE_RESPONSE = "@string/REMOTE_";
|
||||
public static int verbosity = 2;
|
||||
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)
|
||||
@ -134,7 +139,7 @@ namespace Server
|
||||
// Perform a signature verification by signing a nonce
|
||||
switch (cmd[0])
|
||||
{
|
||||
case "Auth":
|
||||
case "Auth": // Log in to a user account (get a session id)
|
||||
{
|
||||
if(!ParseDataPair(cmd[1], out string user, out string pass))
|
||||
{
|
||||
@ -144,7 +149,7 @@ namespace Server
|
||||
Database.User usr = db.GetUser(user);
|
||||
if (usr == null || !usr.Authenticate(pass))
|
||||
{
|
||||
Output.Error("Authentcation failure for user: "+user);
|
||||
if(verbosity > 0) Output.Error("Authentcation failure for user: "+user);
|
||||
return ErrorResponse(id);
|
||||
}
|
||||
|
||||
@ -153,39 +158,59 @@ namespace Server
|
||||
associations["session"] = sess;
|
||||
return GenerateResponse(id, sess);
|
||||
}
|
||||
case "Logout":
|
||||
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":
|
||||
case "Avail": // Check if the given username is available for account registration
|
||||
{
|
||||
try
|
||||
{
|
||||
string name = cmd[1].FromBase64String();
|
||||
string name = cmd[1];
|
||||
Output.Info($"Performing availability check on name \"{name}\"");
|
||||
return GenerateResponse(id, !db.ContainsUser(name));
|
||||
}
|
||||
catch
|
||||
case "Refresh":
|
||||
return GenerateResponse(id, manager.Refresh(cmd[1]));
|
||||
case "List": // List all available users for transactions
|
||||
{
|
||||
Output.Error($"Recieved improperly formatted base64 string: \"{cmd[1]}\"");
|
||||
return GenerateResponse(id, false);
|
||||
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!=user) builder.Append(u.Name.ToBase64String()).Append('&'); return false; });
|
||||
if (builder.Length != 0) --builder.Length;
|
||||
return GenerateResponse(id, builder);
|
||||
}
|
||||
case "Account_Create":
|
||||
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, "badsession");
|
||||
}
|
||||
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 (!ParseDataPair(cmd[1], out string session, out string name) || // Get session id and account name
|
||||
!GetUser(session, out var user) || // Get user associated with session id
|
||||
!GetAccount(name, user, out var account))
|
||||
GetAccount(name, user, out var account))
|
||||
{
|
||||
// Don't print input data to output in case sensitive information was included
|
||||
Output.Error($"Recieved problematic session id or account name!");
|
||||
Output.Error($"Failed to create account \"{name}\" for user \"{manager.GetUser(session)}\" (sessionID={session})");
|
||||
return ErrorResponse(id);
|
||||
}
|
||||
manager.Refresh(session);
|
||||
user.accounts.Add(new Database.Account(user, 0, name));
|
||||
db.UpdateUser(user); // Notify database of the update
|
||||
return GenerateResponse(id, true);
|
||||
}
|
||||
case "Account_Transaction_Create":
|
||||
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;
|
||||
@ -226,6 +251,7 @@ namespace Server
|
||||
// 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,
|
||||
@ -236,7 +262,7 @@ namespace Server
|
||||
data.Length == 6 ? data[5] : null
|
||||
));
|
||||
}
|
||||
case "Account_Close":
|
||||
case "Account_Close": // Close an account if there is no money on the account
|
||||
{
|
||||
Database.User user = null;
|
||||
Database.Account account = null;
|
||||
@ -251,12 +277,13 @@ namespace Server
|
||||
// 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
|
||||
break;
|
||||
return GenerateResponse(id, true);
|
||||
}
|
||||
|
||||
case "Account_Get":
|
||||
case "Account_Get": // Get account info
|
||||
{
|
||||
Database.User user = null;
|
||||
Database.Account account = null;
|
||||
@ -271,15 +298,23 @@ namespace Server
|
||||
// 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);
|
||||
// Response example: "123.45{Sm9obiBEb2U=&Sm9obnMgQWNjb3VudA==&SmFuZSBEb2U=&SmFuZXMgQWNjb3VudA==&123.45&SGV5IHRoZXJlIQ==}"
|
||||
// Exmaple data: balance=123.45, Transaction{to="John Doe", toAccount="Johns Account", from="Jane Doe", fromAccount="Janes Account", amount=123.45, meta="Hey there!"}
|
||||
return GenerateResponse(id, account.ToString());
|
||||
}
|
||||
case "Account_List":
|
||||
case "Account_List": // List accounts associated with a certain user (doesn't give more than account names)
|
||||
{
|
||||
// Get user
|
||||
Database.User user = db.GetUser(cmd[1]);
|
||||
if (user == null) return ErrorResponse(id, "baduser");
|
||||
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();
|
||||
@ -291,17 +326,21 @@ namespace Server
|
||||
// Return accounts
|
||||
return GenerateResponse(id, builder);
|
||||
}
|
||||
case "Reg":
|
||||
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
|
||||
Output.Error($"Recieved problematic username or password!");
|
||||
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)) return ErrorResponse(id, "exists");
|
||||
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);
|
||||
@ -309,16 +348,37 @@ namespace Server
|
||||
|
||||
// Generate a session token
|
||||
string sess = manager.GetSession(u, "ERROR");
|
||||
Output.Positive("Registered account: " + u.Name + "\nSession: "+sess);
|
||||
if(verbosity > 0) Output.Positive("Registered account: " + u.Name + "\nSession: "+sess);
|
||||
associations["session"] = sess;
|
||||
return GenerateResponse(id, sess);
|
||||
}
|
||||
case "Verify":
|
||||
case "PassUPD":
|
||||
{
|
||||
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);
|
||||
return GenerateResponse(id, true);
|
||||
}
|
||||
case "Verify": // Verifies server identity
|
||||
{
|
||||
BitReader bd = new BitReader(Convert.FromBase64String(cmd[1]));
|
||||
try
|
||||
{
|
||||
while (!t.IsCompleted) System.Threading.Thread.Sleep(75);
|
||||
while (!t.IsCompleted && !t.IsFaulted) System.Threading.Thread.Sleep(75);
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
return ErrorResponse(id, "server_err");
|
||||
}
|
||||
byte[] ser;
|
||||
using (BitWriter collector = new BitWriter())
|
||||
{
|
||||
@ -330,10 +390,11 @@ namespace Server
|
||||
}
|
||||
catch
|
||||
{
|
||||
Output.Error("Cryptographic verification failed!");
|
||||
return ErrorResponse(id, "crypterr");
|
||||
}
|
||||
}
|
||||
default:
|
||||
default: // The given message could not be matched to any known endpoint
|
||||
return ErrorResponse(id, "unwn"); // Unknown request
|
||||
}
|
||||
|
||||
@ -341,6 +402,7 @@ namespace Server
|
||||
},
|
||||
(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"]);
|
||||
@ -356,16 +418,35 @@ namespace Server
|
||||
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(
|
||||
.Append(new Command("clear").SetAction(() => Output.Clear()), "Clear screen")
|
||||
.Append(new Command("verb").WithParameter("level", 'l', Parameter.ParamType.STRING, true).SetAction((c, l) =>
|
||||
{
|
||||
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").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('\n');
|
||||
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.Length = builder.Length - 1;
|
||||
else --builder.Insert(0, "Active sessions:\n").Length;
|
||||
Output.Raw(builder);
|
||||
}), "Show active client sessions")
|
||||
}), "List or refresh active client sessions")
|
||||
.Append(new Command("list").WithParameter(Parameter.Flag('a')).SetAction(
|
||||
(c, l) => {
|
||||
bool filter = l.HasFlag('a');
|
||||
@ -405,6 +486,7 @@ namespace Server
|
||||
|
||||
// Set up a persistent terminal-esque input design
|
||||
Output.OnNewLine = () => Output.WriteOverwritable(">> ");
|
||||
Output.Raw(CONSOLE_MOTD);
|
||||
Output.OnNewLine();
|
||||
|
||||
// Server command loop
|
||||
|
@ -44,6 +44,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Command.cs" />
|
||||
<Compile Include="ConsoleReader.cs" />
|
||||
<Compile Include="Database.cs" />
|
||||
<Compile Include="Output.cs" />
|
||||
<Compile Include="CommandHandler.cs" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user