Added: API Heartbeat

This commit is contained in:
TheJoKlLa 2023-01-25 01:48:54 +01:00
parent f78668879b
commit b2cce15464
26 changed files with 522 additions and 304 deletions

View File

@ -1,5 +1,6 @@
using Borepin.Service;
using FabAccessAPI;
using NLog;
using Prism.Navigation;
using Prism.Services;
using System;
@ -30,12 +31,13 @@ namespace Borepin.Base
#endregion
#region Methods
public async void OnConnectionStatusChanged(object sender, ConnectionStatusChange args)
public async void OnConnectionStatusChanged(object sender, ConnectionStatusChanged args)
{
switch(args)
{
case ConnectionStatusChange.Connected:
case ConnectionStatusChanged.Connected:
IsConnected = true;
IsConnecting = false;
try
{
await LoadAPIData().ConfigureAwait(false);
@ -44,35 +46,16 @@ namespace Borepin.Base
{
IsConnected = false;
await _API.Disconnect().ConfigureAwait(false);
_API.UnbindAllEvents();
_API.UnbindEventHandler();
}
break;
case ConnectionStatusChange.Reconnected:
try
{
await ReloadAPIData().ConfigureAwait(false);
}
catch
{
IsConnected = false;
await _API.Disconnect().ConfigureAwait(false);
_API.UnbindAllEvents();
}
break;
case ConnectionStatusChange.ConnectionLoss:
try
{
await _API.Reconnect().ConfigureAwait(false);
}
catch
{
IsConnected = false;
await _API.Disconnect().ConfigureAwait(false);
_API.UnbindAllEvents();
}
break;
case ConnectionStatusChange.Disconnected:
case ConnectionStatusChanged.ConnectionLoss:
IsConnected = false;
IsConnecting = true;
break;
case ConnectionStatusChanged.Disconnected:
IsConnected = false;
IsConnecting = false;
break;
}
}
@ -81,22 +64,28 @@ namespace Borepin.Base
{
return Task.CompletedTask;
}
public virtual Task ReloadAPIData()
{
return Task.CompletedTask;
}
#endregion
#region Fields
/// <summary>
/// PageModel is Connected
/// </summary>
private bool _IsConnected = true;
private bool _IsConnected = false;
public bool IsConnected
{
get => _IsConnected;
set => SetProperty(ref _IsConnected, value);
}
/// <summary>
/// PageModel is Connecting
/// </summary>
private bool _IsConnecting = false;
public bool IsConnecting
{
get => _IsConnecting;
set => SetProperty(ref _IsConnecting, value);
}
#endregion
#region INavigationAware
@ -110,11 +99,12 @@ namespace Borepin.Base
{
await LoadAPIData().ConfigureAwait(false);
}
catch(Exception ex)
catch(Exception exception)
{
IsConnected = false;
await _API.Disconnect().ConfigureAwait(false);
_API.UnbindAllEvents();
_API.UnbindEventHandler();
Log.Error("LoadAPIData failed", exception);
}
}
}

View File

@ -17,9 +17,7 @@
</ContentPage.Resources>
<ContentPage.Content>
<StackLayout Padding="20">
<StackLayout IsVisible="{Binding IsBusy}">
<ActivityIndicator IsRunning="{Binding IsBusy}"></ActivityIndicator>
</StackLayout>
<views:ConnectionStateView/>
<StackLayout IsVisible="{Binding IsBusy, Converter={StaticResource InvertBoolConverter}}">
<StackLayout IsVisible="{Binding IsConnected}">
<Button Text="{x:Static resource_text:TextResource.SCANQR}" Command="{Binding ScanCodeCommand}" Style="{StaticResource Style_Button_Primary}">
@ -48,9 +46,6 @@
</ListView.ItemTemplate>
</ListView>
</StackLayout>
<StackLayout IsVisible="{Binding IsConnected, Converter={StaticResource InvertBoolConverter}}">
<Label Text="{x:Static resource_text:TextResource.PLEASECONNECTTOSERVER}" ></Label>
</StackLayout>
</StackLayout>
</StackLayout>
</ContentPage.Content>

View File

@ -8,6 +8,7 @@ using Borepin.Service;
using Borepin.Service.Storage;
using FabAccessAPI;
using FabAccessAPI.Exceptions;
using FabAccessAPI.Exceptions.SASL;
using Prism.Commands;
using Prism.Navigation;
using Prism.Services;
@ -90,7 +91,7 @@ namespace Borepin.PageModel.AddServerProcess
_ConnectionData = new ConnectionData()
{
Host = _ConnectionData.Host,
Mechanism = Mechanism.PLAIN,
Mechanism = SASLMechanismEnum.PLAIN,
Username = Username,
Properties = new Dictionary<string, object>(StringComparer.Ordinal)
{
@ -104,7 +105,7 @@ namespace Borepin.PageModel.AddServerProcess
if (_API.IsConnected)
{
await _API.Disconnect().ConfigureAwait(true);
_API.UnbindAllEvents();
_API.UnbindEventHandler();
}
}
@ -112,7 +113,7 @@ namespace Borepin.PageModel.AddServerProcess
{
await _API.Connect(_ConnectionData).ConfigureAwait(false);
}
catch (ConnectingFailedException)
catch (ConnectionException)
{
Device.BeginInvokeOnMainThread(async () =>
{

View File

@ -51,7 +51,7 @@ namespace Borepin.PageModel.AddServerProcess
}
public async void AuthPlainCommandExecute()
{
_ConnectionData.Mechanism = Mechanism.PLAIN;
_ConnectionData.Mechanism = SASLMechanismEnum.PLAIN;
INavigationResult result = await _NavigationService.NavigateAsync("AddServerProcess_AuthPlainPage").ConfigureAwait(false);
if(result.Exception != null)

View File

@ -118,9 +118,9 @@ namespace Borepin.PageModel.AddServerProcess
try
{
API api = new API();
await api.TestConnection(_ConnectionData).ConfigureAwait(false);
await api.TryToConnect(_ConnectionData).ConfigureAwait(false);
}
catch(ConnectingFailedException)
catch(ConnectionException)
{
Device.BeginInvokeOnMainThread(async () =>
{

View File

@ -4,6 +4,7 @@ using Borepin.Service;
using Borepin.Service.Storage;
using FabAccessAPI;
using FabAccessAPI.Exceptions;
using FabAccessAPI.Exceptions.SASL;
using Prism.Commands;
using Prism.Navigation;
using Prism.Services;
@ -51,7 +52,7 @@ namespace Borepin.PageModel
throw new InstanceIncorrectException();
}
if(_API.ConnectionData != null && Connection_Item != null)
if(_API.IsConnected && Connection_Item != null)
{
InstanceIsActiveConnection = Connection_Item.Equals(_API.ConnectionData);
}
@ -124,14 +125,14 @@ namespace Borepin.PageModel
if(_API.IsConnected)
{
await _API.Disconnect().ConfigureAwait(true);
_API.UnbindAllEvents();
_API.UnbindEventHandler();
}
try
{
await _API.Connect(Connection_Item).ConfigureAwait(false);
}
catch(ConnectingFailedException)
catch(ConnectionException)
{
Device.BeginInvokeOnMainThread(async () =>
{
@ -184,7 +185,7 @@ namespace Borepin.PageModel
}
public async Task DisonnectCommandExecute()
{
_API.UnbindAllEvents();
_API.UnbindEventHandler();
await _API.Disconnect().ConfigureAwait(false);
await LoadInstance(Connection_Item).ConfigureAwait(false);
@ -215,7 +216,7 @@ namespace Borepin.PageModel
if(string.Equals(result.Parameters.GetValue<string>("result"), "confirm", StringComparison.Ordinal))
{
await _API.Disconnect().ConfigureAwait(false);
_API.UnbindAllEvents();
_API.UnbindEventHandler();
await _LoginStorageService.Remove(result.Parameters.GetValue<ConnectionData>("instance")).ConfigureAwait(false);
Device.BeginInvokeOnMainThread(async () =>

View File

@ -3,6 +3,7 @@ using Borepin.Service;
using Borepin.Service.Storage;
using FabAccessAPI;
using FabAccessAPI.Exceptions;
using FabAccessAPI.Exceptions.SASL;
using Prism.AppModel;
using Prism.Navigation;
using Prism.Services;
@ -69,7 +70,7 @@ namespace Borepin.PageModel
}
});
}
catch (ConnectingFailedException)
catch (ConnectionException)
{
Device.BeginInvokeOnMainThread(async () =>
{

View File

@ -16,9 +16,11 @@
<ActivityIndicator IsRunning="{Binding IsBusy}"></ActivityIndicator>
</StackLayout>
<StackLayout IsVisible="{Binding IsBusy, Converter={StaticResource InvertBoolConverter}}">
<Label Text="No Connection to Server" IsVisible="{Binding IsConnected, Converter={StaticResource InvertBoolConverter}}"/>
<Label Text="Reconnecting to Server ..." IsVisible="{Binding IsReconnecting}"/>
<Label Text="Please connect to Server." IsVisible="{Binding IsReconnecting, Converter={StaticResource InvertBoolConverter}}"/>
<StackLayout IsVisible="{Binding IsConnected, Converter={StaticResource InvertBoolConverter}}">
<Label Text="No Connection to Server"/>
<Label Text="Connecting to Server ..." IsVisible="{Binding IsConnecting}"/>
<Label Text="Please select a Server." IsVisible="{Binding IsConnecting, Converter={StaticResource InvertBoolConverter}}"/>
</StackLayout>
</StackLayout>
</StackLayout>
</ContentView.Content>

View File

@ -1,5 +1,6 @@
using Capnp.Rpc;
using FabAccessAPI.Exceptions;
using FabAccessAPI.Exceptions.SASL;
using FabAccessAPI.Schema;
using NLog;
using S22.Sasl;
@ -7,7 +8,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
@ -21,49 +21,76 @@ namespace FabAccessAPI
#endregion
#region Private Members
/// <summary>
/// Internal client to connect to a server with TCP and RPC
/// </summary>
private TcpRpcClient _TcpRpcClient;
/// <summary>
/// Private ConnectionData
/// </summary>
private ConnectionData _ConnectionData;
/// <summary>
/// Private ServerData
/// </summary>
private ServerData _ServerData;
/// <summary>
/// Private Session
/// </summary>
private Session _Session;
/// <summary>
/// Private Bootstrap
/// </summary>
private IBootstrap _Bootstrap;
/// <summary>
/// Timer to check connection status
/// </summary>
private readonly Timer _ConnectionHeatbeat;
/// <summary>
/// Semaphore to connect only once
/// </summary>
private static SemaphoreSlim _ConnectSemaphore = new SemaphoreSlim(1, 1);
private static SemaphoreSlim _ReconnectSemaphore = new SemaphoreSlim(1, 1);
#endregion
#region Constructors
public API()
{
}
#endregion
#region Events
public event EventHandler<ConnectionStatusChange> ConnectionStatusChanged;
public void OnTcpRpcConnectionChanged(object sender, ConnectionStateChange args)
{
if (args.LastState == ConnectionState.Active && args.NewState == ConnectionState.Down)
{
Log.Trace("TcpRpcClient Event ConnectionLoss");
ConnectionStatusChanged?.Invoke(this, ConnectionStatusChange.ConnectionLoss);
_TcpRpcClient = null;
}
}
public void UnbindAllEvents()
{
if(ConnectionStatusChanged != null)
{
foreach (Delegate d in ConnectionStatusChanged.GetInvocationList())
{
ConnectionStatusChanged -= (EventHandler<ConnectionStatusChange>)d;
}
}
_ConnectionHeatbeat = new Timer(Heartbeat, null, 1000, 1000);
}
#endregion
#region Members
public ConnectionData ConnectionData { get; private set; }
/// <summary>
/// State of the conneciton, can the API-Service connect to a server
/// </summary>
public bool CanConnect
{
get
{
return _ConnectionData != null;
}
}
public ConnectionInfo ConnectionInfo { get; private set; }
/// <summary>
/// State of the conneciton, is the API-Service connecting to a server
/// </summary>
public bool IsConnecting
{
get
{
return _TcpRpcClient != null && _ConnectionData != null;
}
}
/// <summary>
/// State of the conneciton, is the API-Service connected to a server
/// </summary>
public bool IsConnected
{
get
@ -72,15 +99,124 @@ namespace FabAccessAPI
}
}
public Session Session { get; private set; }
/// <summary>
/// Information about the connection
/// </summary>
/// <exception cref="InvalidOperationException"> When API-Service is not connected or trying to connected to a server </exception>
public ConnectionData ConnectionData
{
get
{
if(_ConnectionData == null || !IsConnecting)
{
throw new InvalidOperationException();
}
else
{
return _ConnectionData;
}
}
private set
{
_ConnectionData = value;
}
}
/// <summary>
/// Information about the server
/// Is only avalible if the API-Service is connected
/// </summary>
/// <exception cref="InvalidOperationException"> When API-Service is not connected </exception>
public ServerData ServerData
{
get
{
if (_ServerData == null || !IsConnected)
{
throw new InvalidOperationException();
}
else
{
return _ServerData;
}
}
private set
{
_ServerData = value;
}
}
#endregion
#region Events
/// <summary>
/// Event on changes in connection status
/// </summary>
public event EventHandler<ConnectionStatusChanged> ConnectionStatusChanged;
/// <summary>
/// Unbind all handlers from EventHandler<ConnectionStatusChanged>
/// </summary>
public void UnbindEventHandler()
{
if (ConnectionStatusChanged != null)
{
Log.Trace("Eventhandlers unbinded");
foreach (Delegate d in ConnectionStatusChanged.GetInvocationList())
{
ConnectionStatusChanged -= (EventHandler<ConnectionStatusChanged>)d;
}
}
}
/// <summary>
/// Eventhandler for TcpRpcConnectionChanged
/// Track connection loss and publish i in ConnectionStatusChanged
/// </summary>
public void OnTcpRpcConnectionChanged(object sender, ConnectionStateChange args)
{
if (args.LastState == ConnectionState.Active && args.NewState == ConnectionState.Down)
{
Log.Trace("TcpRpcClient Event ConnectionLoss");
ConnectionStatusChanged?.Invoke(this, FabAccessAPI.ConnectionStatusChanged.ConnectionLoss);
}
}
#endregion
#region Session
/// <summary>
/// Get session after connection
/// </summary>
/// <exception cref="InvalidOperationException"> When API-Service is not connected </exception>
public Session Session
{
get
{
if (_Session == null || !IsConnected)
{
throw new InvalidOperationException();
}
else
{
return _Session;
}
}
private set
{
_Session = value;
}
}
#endregion
#region Methods
/// <summary>
/// Connect to server with ConnectionData
/// If connection lost, the API-Server will try to reconnect
/// </summary>
/// <exception cref="AuthenticationException"></exception>
/// <exception cref="ConnectingFailedException"></exception>
/// <param name="connectionData"> Data to establish a connection to a server </param>
/// <exception cref="ConnectionException"> When API-Service can not connect to a server </exception>
/// <exception cref="AuthenticationException"> When API-Service can connect to a server but can not authenticate </exception>
/// <exception cref="InvalidOperationException"> When API-Service is allready connected </exception>
public async Task Connect(ConnectionData connectionData, TcpRpcClient tcpRpcClient = null)
{
await _ConnectSemaphore.WaitAsync();
@ -88,7 +224,8 @@ namespace FabAccessAPI
{
if (IsConnected)
{
await Disconnect();
Log.Warn("API already connected");
throw new InvalidOperationException();
}
if (tcpRpcClient == null)
@ -101,20 +238,19 @@ namespace FabAccessAPI
await _ConnectAsync(tcpRpcClient, connectionData).ConfigureAwait(false);
_Bootstrap = tcpRpcClient.GetMain<IBootstrap>();
ConnectionInfo = await _GetConnectionInfo(_Bootstrap);
ServerData = await _GetServerData(_Bootstrap);
Session = await _Authenticate(connectionData).ConfigureAwait(false);
ConnectionData = connectionData;
_TcpRpcClient = tcpRpcClient;
tcpRpcClient.ConnectionStateChanged += OnTcpRpcConnectionChanged;
ConnectionStatusChanged?.Invoke(this, ConnectionStatusChange.Connected);
ConnectionStatusChanged?.Invoke(this, FabAccessAPI.ConnectionStatusChanged.Connected);
Log.Info("API connected");
}
catch (System.Exception ex)
{
await Disconnect().ConfigureAwait(false);
Log.Warn(ex, "API connecting failed");
Log.Warn(ex, "API connect failed");
throw ex;
}
}
@ -124,47 +260,36 @@ namespace FabAccessAPI
}
}
/// <summary>
/// Disconnect from a server
/// </summary>
/// <exception cref="InvalidOperationException"> When API-Service is not connected or trying to connect </exception>
public Task Disconnect()
{
if (IsConnected)
{
_TcpRpcClient.Dispose();
}
_Bootstrap = null;
Session = null;
_TcpRpcClient = null;
ConnectionData = null;
ConnectionInfo = null;
ConnectionStatusChanged?.Invoke(this, ConnectionStatusChange.Disconnected);
_Bootstrap = null;
_TcpRpcClient = null;
Session = null;
ConnectionData = null;
ServerData = null;
ConnectionStatusChanged?.Invoke(this, FabAccessAPI.ConnectionStatusChanged.Disconnected);
Log.Info("API disconnected");
return Task.CompletedTask;
}
public async Task Reconnect()
{
await _ReconnectSemaphore.WaitAsync();
try
{
if (ConnectionData != null && IsConnected == false)
{
await Connect(ConnectionData);
}
ConnectionStatusChanged?.Invoke(this, ConnectionStatusChange.Reconnected);
Log.Info("API reconnected");
}
finally
{
_ReconnectSemaphore.Release();
}
}
public async Task<ConnectionInfo> TestConnection(ConnectionData connectionData, TcpRpcClient tcpRpcClient = null)
/// <summary>
/// Try to connect to a server and get ServerData
/// The connection is not maintained
/// </summary>
/// <exception cref="ConnectionException"> When API-Service can not connect to a server </exception>
public async Task<ServerData> TryToConnect(ConnectionData connectionData, TcpRpcClient tcpRpcClient = null)
{
try
{
@ -174,41 +299,57 @@ namespace FabAccessAPI
}
await _ConnectAsync(tcpRpcClient, connectionData).ConfigureAwait(false);
IBootstrap testBootstrap = tcpRpcClient.GetMain<IBootstrap>();
IBootstrap bootstrap = tcpRpcClient.GetMain<IBootstrap>();
ConnectionInfo connectionInfo = await _GetConnectionInfo(testBootstrap).ConfigureAwait(false);
ServerData serverData = await _GetServerData(bootstrap).ConfigureAwait(false);
tcpRpcClient.Dispose();
return connectionInfo;
return serverData;
}
catch
catch(System.Exception ex)
{
throw new ConnectingFailedException();
throw new ConnectionException("Test Connection Failed", ex);
}
}
/// <summary>
/// Public Wrapper to run HeartbeatAsync
/// </summary>
public void Heartbeat(object state)
{
_ = HeartbeatAsync();
}
#endregion
#region Private Methods
private async Task HeartbeatAsync()
{
if(!IsConnected && CanConnect)
{
await Connect(ConnectionData).ConfigureAwait(false);
}
}
/// <summary>
/// Validate Certificate
/// TODO: Do some validation
/// </summary>
private static bool _RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
private bool _RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// TODO Cert Check
return true;
}
/// <summary>
/// Connect to server async with ConnectionData
/// Connect async to a server with ConnectionData
/// </summary>
/// <exception cref="AuthenticationException">TLS Error</exception>
/// <exception cref="ConnectingFailedException">Based on RPC Exception</exception>
///
private async Task _ConnectAsync(TcpRpcClient rpcClient, ConnectionData connectionData)
/// <exception cref="ConnectionException">Based on RPC Exception</exception>
private async Task _ConnectAsync(TcpRpcClient tcprpcClient, ConnectionData connectionData)
{
rpcClient.InjectMidlayer((tcpstream) =>
tcprpcClient.InjectMidlayer((tcpstream) =>
{
var sslStream = new SslStream(tcpstream, false, new RemoteCertificateValidationCallback(_RemoteCertificateValidationCallback));
try
@ -216,61 +357,58 @@ namespace FabAccessAPI
sslStream.AuthenticateAsClient("bffhd");
return sslStream;
}
catch (AuthenticationException)
catch (AuthenticationException exception)
{
sslStream.Close();
throw;
Log.Warn(exception);
throw new ConnectionException("TLS failed", exception);
}
});
try
{
Task timeoutTask = Task.Delay(3000);
rpcClient.Connect(connectionData.Host.Host, connectionData.Host.Port);
await await Task.WhenAny(rpcClient.WhenConnected, timeoutTask);
if(timeoutTask.IsCompleted)
tcprpcClient.Connect(connectionData.Host.Host, connectionData.Host.Port);
await await Task.WhenAny(tcprpcClient.WhenConnected, timeoutTask);
if (timeoutTask.IsCompleted)
{
throw new ConnectingFailedException("Connection timeout");
Exceptions.TimeoutException timeoutException = new Exceptions.TimeoutException();
Log.Warn(timeoutException);
throw new ConnectionException("Connection timeout", timeoutException);
}
}
catch (RpcException exception) when (string.Equals(exception.Message, "TcpRpcClient is unable to connect", StringComparison.Ordinal))
{
throw new ConnectingFailedException("RPC Connecting failed", exception);
Log.Warn(exception);
throw new ConnectionException("RPC Connecting failed", exception);
}
}
/// <summary>
/// Create ConnectionInfo from Bootstrap
/// </summary>
private async Task<ConnectionInfo> _GetConnectionInfo(IBootstrap bootstrap)
{
ConnectionInfo connectionInfo = new ConnectionInfo()
{
APIVersion = await bootstrap.GetAPIVersion().ConfigureAwait(false),
Mechanisms = new List<string>(await bootstrap.Mechanisms().ConfigureAwait(false)),
ServerName = (await bootstrap.GetServerRelease().ConfigureAwait(false)).Item1,
ServerRelease = (await bootstrap.GetServerRelease().ConfigureAwait(false)).Item2,
};
return connectionInfo;
}
/// <summary>
/// Authenticate connection with ConnectionData
/// </summary>
/// <exception cref="UnsupportedMechanismException"></exception>
/// <exception cref="InvalidCredentialsException"></exception>
/// <exception cref="AuthenticationFailedException"></exception>
/// <param name="connectionData"> Data to establish a connection to a server </param>
/// <exception cref="AuthenticationException"></exception>
private async Task<Session> _Authenticate(ConnectionData connectionData)
{
IAuthentication? authentication = await _Bootstrap.CreateSession(MechanismString.ToString(connectionData.Mechanism)).ConfigureAwait(false);
IAuthentication? authentication = await _Bootstrap.CreateSession(SASLMechanism.ToString(connectionData.Mechanism)).ConfigureAwait(false);
return await _SASLAuthenticate(authentication, MechanismString.ToString(connectionData.Mechanism), connectionData.Properties).ConfigureAwait(false);
try
{
return await _SASLAuthenticate(authentication, SASLMechanism.ToString(connectionData.Mechanism), connectionData.Properties).ConfigureAwait(false);
}
catch (System.Exception exception)
{
Log.Warn(exception, "API authenticating failed");
AuthenticationException authenticationException = new AuthenticationException("Authentication failed", exception);
throw authenticationException;
}
}
/// <summary>
/// Authenticate Connection to get Session
/// Authenticate with SASL
/// </summary>
/// <exception cref="BadMechanismException"></exception>
/// <exception cref="InvalidCredentialsException"></exception>
@ -293,11 +431,11 @@ namespace FabAccessAPI
Response? response = await authentication.Step(data);
while (!saslMechanism.IsCompleted)
{
if(response.Failed != null)
if (response.Failed != null)
{
break;
}
if(response.Challenge != null)
if (response.Challenge != null)
{
byte[]? additional = saslMechanism.GetResponse(response.Challenge.ToArray());
response = await authentication.Step(additional);
@ -331,6 +469,22 @@ namespace FabAccessAPI
throw new AuthenticationFailedException();
}
}
/// <summary>
/// Get ServerData from server with tcprpcconnection
/// </summary>
private async Task<ServerData> _GetServerData(IBootstrap bootstrap)
{
ServerData serverData = new ServerData()
{
APIVersion = await bootstrap.GetAPIVersion().ConfigureAwait(false),
Mechanisms = new List<string>(await bootstrap.Mechanisms().ConfigureAwait(false)),
ServerName = (await bootstrap.GetServerRelease().ConfigureAwait(false)).Item1,
ServerRelease = (await bootstrap.GetServerRelease().ConfigureAwait(false)).Item2,
};
return serverData;
}
#endregion
}
}

View File

@ -3,12 +3,18 @@ using System.Collections.Generic;
namespace FabAccessAPI
{
/// <summary>
/// Data to establish a connection to a server
/// With Data for Authentication
/// </summary>
public class ConnectionData
{
public Uri Host;
public Mechanism Mechanism;
public string Username;
public SASLMechanismEnum Mechanism;
public Dictionary<string, object> Properties;
public DateTime LastTime;
public override bool Equals(object? obj)

View File

@ -1,23 +0,0 @@
namespace FabAccessAPI
{
public enum ConnectionStatusChange
{
/// <summary>
/// Client has established connection to server.
/// </summary>
Connected,
/// <summary>
/// Client has closed connection to server.
/// </summary>
Disconnected,
/// <summary>
/// Connection was lost and Client has reestablished connection to server.
/// </summary>
Reconnected,
/// <summary>
/// Connection was lost and can be reestablished with reconnect
/// Connection should be closed if reconnecting fails.
/// </summary>
ConnectionLoss
}
}

View File

@ -0,0 +1,20 @@
namespace FabAccessAPI
{
public enum ConnectionStatusChanged
{
/// <summary>
/// API-Service has established connection to server
/// </summary>
Connected,
/// <summary>
/// API-Service has closed the connection to a server
/// </summary>
Disconnected,
/// <summary>
/// Connection was lost and the API-Service will try to reconnect automatically
/// </summary>
ConnectionLoss
}
}

View File

@ -0,0 +1,26 @@
using System;
namespace FabAccessAPI.Exceptions
{
/// <summary>
/// Authenticating to a server has failed
/// InnerException will provide more information
/// </summary>
public class AuthenticationException : Exception
{
public AuthenticationException()
{
}
public AuthenticationException(string message) : base(message)
{
}
public AuthenticationException(string message, Exception inner) : base(message, inner)
{
}
}
}

View File

@ -1,22 +0,0 @@
using System;
namespace FabAccessAPI.Exceptions
{
public class ConnectingFailedException : Exception
{
public ConnectingFailedException()
{
}
public ConnectingFailedException(string message) : base(message)
{
}
public ConnectingFailedException(string message, Exception inner) : base(message, inner)
{
}
}
}

View File

@ -0,0 +1,26 @@
using System;
namespace FabAccessAPI.Exceptions
{
/// <summary>
/// Connecting to a server has failed
/// InnerException will provide more information
/// </summary>
public class ConnectionException : Exception
{
public ConnectionException()
{
}
public ConnectionException(string message) : base(message)
{
}
public ConnectionException(string message, Exception inner) : base(message, inner)
{
}
}
}

View File

@ -1,6 +1,6 @@
using System;
namespace FabAccessAPI.Exceptions
namespace FabAccessAPI.Exceptions.SASL
{
public class AuthenticationFailedException : Exception
{

View File

@ -1,6 +1,6 @@
using System;
namespace FabAccessAPI.Exceptions
namespace FabAccessAPI.Exceptions.SASL
{
public class BadMechanismException : Exception
{

View File

@ -1,6 +1,6 @@
using System;
namespace FabAccessAPI.Exceptions
namespace FabAccessAPI.Exceptions.SASL
{
public class InvalidCredentialsException : Exception
{

View File

@ -0,0 +1,25 @@
using System;
namespace FabAccessAPI.Exceptions
{
/// <summary>
/// Timeout on Connection
/// </summary>
public class TimeoutException : Exception
{
public TimeoutException()
{
}
public TimeoutException(string message) : base(message)
{
}
public TimeoutException(string message, Exception inner) : base(message, inner)
{
}
}
}

View File

@ -1,63 +1,84 @@
using Capnp.Rpc;
using FabAccessAPI.Schema;
using System;
using System;
using System.Threading.Tasks;
using Capnp.Rpc;
using FabAccessAPI.Exceptions;
using FabAccessAPI.Schema;
namespace FabAccessAPI
{
/// <summary>
/// Service to connect to a server and maintain the connection
/// </summary>
public interface IAPI
{
#region Information about a connection and the server
/// <summary>
/// Data to establish connection.
/// State of the conneciton, is the API-Service connecting to a server
/// </summary>
/// Without SecretProperties
ConnectionData ConnectionData { get; }
bool IsConnecting { get; }
/// <summary>
/// Information about the established connection.
/// </summary>
ConnectionInfo ConnectionInfo { get; }
/// <summary>
/// Is API connected?
/// State of the conneciton, is the API-Service connected to a server
/// </summary>
bool IsConnected { get; }
/// <summary>
/// Get session when connection is established
/// Information about the connection
/// </summary>
Session Session { get; }
/// <exception cref="InvalidOperationException"> When API-Service is not connected or trying to connected to a server </exception>
ConnectionData ConnectionData { get; }
/// <summary>
/// Event on changes in connection state.
/// Information about the server
/// Is only avalible if the API-Service is connected
/// </summary>
event EventHandler<ConnectionStatusChange> ConnectionStatusChanged;
/// <exception cref="InvalidOperationException"> When API-Service is not connected </exception>
ServerData ServerData { get; }
#endregion
#region Methods to connect to server
/// <summary>
/// Unbind all Events from ConnectionStatus Change
/// Connect to server with ConnectionData
/// If connection lost, the API-Server will try to reconnect
/// </summary>
void UnbindAllEvents();
/// <summary>
/// Connect to BFFH Server
/// </summary>
/// <param name="connectionData"></param>
/// <param name="connectionData"> Data to establish a connection to a server </param>
/// <exception cref="ConnectionException"> When API-Service can not connect to a server </exception>
/// <exception cref="AuthenticationException"> When API-Service can connect to a server but can not authenticate </exception>
/// <exception cref="InvalidOperationException"> When API-Service is allready connected </exception>
Task Connect(ConnectionData connectionData, TcpRpcClient tcpRpcClient = null);
/// <summary>
/// Disconnect from BFFH Server
/// Disconnect from a server
/// </summary>
/// <exception cref="InvalidOperationException"> When API-Service is not connected or trying to connect </exception>
Task Disconnect();
/// <summary>
/// Reconnect after connection loss with the last ConnectionData
/// Try to connect to a server and get ServerData
/// The connection is not maintained
/// </summary>
Task Reconnect();
/// <exception cref="ConnectionException"> When API-Service can not connect to a server </exception>
Task<ServerData> TryToConnect(ConnectionData connectionData, TcpRpcClient tcpRpcClient = null);
#endregion
#region Session
/// <summary>
/// Get session after connection
/// </summary>
/// <exception cref="InvalidOperationException"> When API-Service is not connected </exception>
Session Session { get; }
#endregion
#region Events
/// <summary>
/// Event on changes in connection status
/// </summary>
event EventHandler<ConnectionStatusChanged> ConnectionStatusChanged;
/// <summary>
/// Connect to Server and get ConnectionInfo.
/// The Connection is not maintained.
/// Unbind all handlers from EventHandler<ConnectionStatusChanged>
/// </summary>
Task<ConnectionInfo> TestConnection(ConnectionData connectionData, TcpRpcClient tcpRpcClient = null);
void UnbindEventHandler();
#endregion
}
}

View File

@ -1,24 +0,0 @@
using System;
namespace FabAccessAPI
{
public enum Mechanism
{
PLAIN,
}
public static class MechanismString
{
public static string ToString(Mechanism mechanism)
{
switch(mechanism)
{
case Mechanism.PLAIN:
return "PLAIN";
default:
throw new ArgumentException("Mechanism not known.");
}
}
}
}

View File

@ -0,0 +1,24 @@
using System;
namespace FabAccessAPI
{
public enum SASLMechanismEnum
{
PLAIN,
}
public static class SASLMechanism
{
public static string ToString(SASLMechanismEnum mechanism)
{
switch(mechanism)
{
case SASLMechanismEnum.PLAIN:
return "PLAIN";
default:
throw new ArgumentException("Mechanism unknown.");
}
}
}
}

View File

@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace FabAccessAPI
{
public class ConnectionInfo
public class ServerData
{
public Schema.Version APIVersion;
public string ServerName;

View File

@ -1,6 +1,7 @@
using Capnp.Rpc;
using FabAccessAPI;
using FabAccessAPI.Exceptions;
using FabAccessAPI.Exceptions.SASL;
using NUnit.Framework;
using System;
using System.Collections.Generic;
@ -19,7 +20,7 @@ namespace FabAccessAPI_Test
ConnectionData connectionData = new ConnectionData()
{
Host = new UriBuilder(TestEnv.SCHEMA, "UnkownHost" + TestEnv.TESTSERVER, TestEnv.TESTSERVER_PORT).Uri,
Mechanism = Mechanism.PLAIN,
Mechanism = SASLMechanismEnum.PLAIN,
Username = "UnkownUser",
Properties = new Dictionary<string, object>()
{
@ -32,14 +33,14 @@ namespace FabAccessAPI_Test
{
await api.Connect(connectionData);
}
catch (ConnectingFailedException)
catch (ConnectionException)
{
Assert.Pass();
}
Assert.Fail();
}
[Test, Ignore("")]
[Test]
public async Task Connect_InvalidCredentials()
{
API api = new API();
@ -50,7 +51,7 @@ namespace FabAccessAPI_Test
{
await api.Connect(connectionData);
}
catch(InvalidCredentialsException)
catch(AuthenticationException exception) when (exception.InnerException is InvalidCredentialsException)
{
Assert.Pass();
}
@ -68,11 +69,11 @@ namespace FabAccessAPI_Test
bool event_Disconnected = false;
api.ConnectionStatusChanged += (sender, eventArgs) =>
{
if (eventArgs == ConnectionStatusChange.Connected)
if (eventArgs == ConnectionStatusChanged.Connected)
{
event_Connected = true;
}
if(eventArgs == ConnectionStatusChange.Disconnected)
if(eventArgs == ConnectionStatusChanged.Disconnected)
{
event_Disconnected = true;
}
@ -82,7 +83,7 @@ namespace FabAccessAPI_Test
bool HasSesion = api.Session != null;
bool HasConnectionData = api.ConnectionData != null;
bool HasConnectionInfo = api.ConnectionInfo != null;
bool HasServerData = api.ServerData != null;
bool IsConnected = api.IsConnected;
await api.Disconnect();
@ -95,7 +96,7 @@ namespace FabAccessAPI_Test
Assert.IsTrue(HasSesion, "HasSesion");
Assert.IsTrue(HasConnectionData, "HasConnectionData");
Assert.IsTrue(HasConnectionInfo, "HasConnectionInfo");
Assert.IsTrue(HasServerData, "HasServerData");
Assert.IsTrue(IsConnected, "IsConnected");
Assert.IsFalse(api.IsConnected, "api.IsConnected");
@ -109,9 +110,9 @@ namespace FabAccessAPI_Test
ConnectionData connectionData = TestEnv.CreateConnetionData(username);
ConnectionInfo connectionInfo = await api.TestConnection(connectionData);
ServerData serverData = await api.TryToConnect(connectionData);
Assert.IsNotNull(connectionInfo);
Assert.IsNotNull(serverData);
}
[TestCase("Admin1"), Explicit]
@ -121,28 +122,23 @@ namespace FabAccessAPI_Test
ConnectionData connectionData = TestEnv.CreateConnetionData(username);
bool event_Connected = false;
bool event_ConnectionLoss = false;
bool event_Reconnect = false;
bool event_Disconnected = false;
int event_Connected = 0;
int event_ConnectionLoss = 0;
int event_Disconnected = 0;
api.ConnectionStatusChanged += (sender, eventArgs) =>
{
if (eventArgs == ConnectionStatusChange.Connected)
if (eventArgs == ConnectionStatusChanged.Connected)
{
event_Connected = true;
event_Connected++;
}
if (eventArgs == ConnectionStatusChange.ConnectionLoss)
if (eventArgs == ConnectionStatusChanged.ConnectionLoss)
{
event_ConnectionLoss = true;
event_ConnectionLoss++;
}
if (eventArgs == ConnectionStatusChange.Reconnected)
if (eventArgs == ConnectionStatusChanged.Disconnected)
{
event_Reconnect = true;
}
if (eventArgs == ConnectionStatusChange.Disconnected)
{
event_Disconnected = true;
event_Disconnected++;
}
};
@ -161,16 +157,14 @@ namespace FabAccessAPI_Test
Thread.Sleep(3000);
// Stop here and connect with internet again
await api.Reconnect();
await api.Disconnect();
Thread.Sleep(3000);
Thread.Sleep(1000);
Assert.Multiple(() =>
{
Assert.IsTrue(event_Connected, "event_Connected");
Assert.IsTrue(event_ConnectionLoss, "event_ConnectionLoss");
Assert.IsTrue(event_Reconnect, "event_Reconnect");
Assert.IsTrue(event_Disconnected, "event_Disconnected");
Assert.AreEqual(2, event_Connected, "event_Connected");
Assert.AreEqual(1, event_ConnectionLoss, "event_ConnectionLoss");
Assert.AreEqual(1, event_Disconnected, "event_Disconnected");
});
}

View File

@ -34,6 +34,7 @@ namespace FabAccessAPI_Test.API_TestEnv
[TestCase("Admin1", "NewMakerA1", "UseA", "ReadA", "DiscloseA")]
[Order(1)]
[Ignore("Deprecated")]
public async Task AddRoles(string username, string username2, params string[] roles)
{
API api = new API();

View File

@ -7,8 +7,8 @@ namespace FabAccessAPI_Test
public static class TestEnv
{
public const string SCHEMA = "fabaccess";
public const string TESTSERVER = "127.0.0.1";//"bffh.lab.bln.kjknet.de";
public const int TESTSERVER_PORT = 59666;
public const string TESTSERVER = "127.0.0.1";//"test.fab-access.org";
public const int TESTSERVER_PORT = 59661;
public const string PASSWORD = "secret";
public static ConnectionData CreateConnetionData(string username)
@ -16,7 +16,7 @@ namespace FabAccessAPI_Test
ConnectionData connectionData = new ConnectionData()
{
Host = new UriBuilder(TestEnv.SCHEMA, TestEnv.TESTSERVER, TestEnv.TESTSERVER_PORT).Uri,
Mechanism = Mechanism.PLAIN,
Mechanism = SASLMechanismEnum.PLAIN,
Username = username,
Properties = new Dictionary<string, object>()
{