507 lines
17 KiB
C#
Raw Permalink Normal View History

2022-05-11 15:02:17 +02:00
using Capnp.Rpc;
using FabAccessAPI.Exceptions;
2023-01-25 01:48:54 +01:00
using FabAccessAPI.Exceptions.SASL;
2022-05-11 15:02:17 +02:00
using FabAccessAPI.Schema;
2022-05-16 22:41:29 +02:00
using NLog;
2022-05-12 23:08:37 +02:00
using S22.Sasl;
2022-05-10 13:35:23 +02:00
using System;
2022-05-11 15:02:17 +02:00
using System.Collections.Generic;
2023-01-31 14:14:33 +01:00
using System.IO;
2022-05-12 23:08:37 +02:00
using System.Linq;
2022-05-11 15:02:17 +02:00
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
2022-05-19 17:47:49 +02:00
using System.Threading;
2022-05-11 15:02:17 +02:00
using System.Threading.Tasks;
2022-05-10 13:35:23 +02:00
namespace FabAccessAPI
{
public class API : IAPI
{
2022-05-16 22:41:29 +02:00
#region Logger
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
#endregion
2022-05-10 13:35:23 +02:00
#region Private Members
2023-01-25 01:48:54 +01:00
/// <summary>
/// Internal client to connect to a server with TCP and RPC
/// </summary>
2022-05-12 23:08:37 +02:00
private TcpRpcClient _TcpRpcClient;
2023-01-25 01:48:54 +01:00
/// <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>
2022-05-12 23:08:37 +02:00
private IBootstrap _Bootstrap;
2023-01-25 01:48:54 +01:00
/// <summary>
/// Timer to check connection status
/// </summary>
private readonly Timer _ConnectionHeatbeat;
/// <summary>
/// Semaphore to connect only once
/// </summary>
2022-05-19 17:47:49 +02:00
private static SemaphoreSlim _ConnectSemaphore = new SemaphoreSlim(1, 1);
2022-05-10 13:35:23 +02:00
#endregion
#region Constructors
2023-01-25 01:48:54 +01:00
2022-05-10 13:35:23 +02:00
public API()
{
2023-01-25 01:48:54 +01:00
_ConnectionHeatbeat = new Timer(Heartbeat, null, 1000, 1000);
2022-05-10 13:35:23 +02:00
}
#endregion
2022-05-10 23:50:04 +02:00
2023-01-25 01:48:54 +01:00
#region Members
/// <summary>
/// State of the conneciton, can the API-Service connect to a server
/// </summary>
public bool CanConnect
{
get
{
return _ConnectionData != null;
}
}
2022-05-12 23:08:37 +02:00
2023-01-25 01:48:54 +01:00
/// <summary>
/// State of the conneciton, is the API-Service connecting to a server
/// </summary>
public bool IsConnecting
2022-05-12 23:08:37 +02:00
{
2023-01-25 01:48:54 +01:00
get
2022-05-12 23:08:37 +02:00
{
2023-01-25 01:48:54 +01:00
return _TcpRpcClient != null && _ConnectionData != null;
2022-05-12 23:08:37 +02:00
}
}
2022-05-17 23:23:47 +02:00
2023-01-25 01:48:54 +01:00
/// <summary>
/// State of the conneciton, is the API-Service connected to a server
/// </summary>
public bool IsConnected
2022-05-17 23:23:47 +02:00
{
2023-01-25 01:48:54 +01:00
get
2022-05-17 23:23:47 +02:00
{
2023-01-25 01:48:54 +01:00
return _TcpRpcClient != null && _TcpRpcClient.State == ConnectionState.Active;
}
}
/// <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)
2022-05-17 23:23:47 +02:00
{
2023-01-25 01:48:54 +01:00
throw new InvalidOperationException();
2022-05-17 23:23:47 +02:00
}
2023-01-25 01:48:54 +01:00
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;
2022-05-17 23:23:47 +02:00
}
}
2022-05-10 13:35:23 +02:00
#endregion
2023-01-25 01:48:54 +01:00
#region Events
/// <summary>
/// Event on changes in connection status
/// </summary>
public event EventHandler<ConnectionStatusChanged> ConnectionStatusChanged;
2022-05-10 13:35:23 +02:00
2023-01-25 01:48:54 +01:00
/// <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;
}
}
}
2022-05-10 13:35:23 +02:00
2023-01-25 01:48:54 +01:00
/// <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
2022-05-10 13:35:23 +02:00
{
get
{
2023-01-25 01:48:54 +01:00
if (_Session == null || !IsConnected)
{
throw new InvalidOperationException();
}
else
{
return _Session;
}
}
private set
{
_Session = value;
2022-05-10 13:35:23 +02:00
}
}
#endregion
#region Methods
2022-05-11 15:02:17 +02:00
/// <summary>
/// Connect to server with ConnectionData
2023-01-25 01:48:54 +01:00
/// If connection lost, the API-Server will try to reconnect
2022-05-11 15:02:17 +02:00
/// </summary>
2023-01-25 01:48:54 +01:00
/// <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>
2022-05-12 23:08:37 +02:00
public async Task Connect(ConnectionData connectionData, TcpRpcClient tcpRpcClient = null)
2022-05-10 13:35:23 +02:00
{
2022-05-19 17:47:49 +02:00
await _ConnectSemaphore.WaitAsync();
try
2022-05-11 15:02:17 +02:00
{
2022-05-19 17:47:49 +02:00
if (IsConnected)
{
2023-01-25 01:48:54 +01:00
Log.Warn("API already connected");
throw new InvalidOperationException();
2022-05-19 17:47:49 +02:00
}
2022-05-11 15:02:17 +02:00
2022-05-19 17:47:49 +02:00
if (tcpRpcClient == null)
{
2022-05-29 19:05:20 +02:00
tcpRpcClient = new TcpRpcClient();
2022-05-19 17:47:49 +02:00
}
2022-05-11 15:02:17 +02:00
2022-05-19 17:47:49 +02:00
try
{
await _ConnectAsync(tcpRpcClient, connectionData).ConfigureAwait(false);
2022-05-12 23:08:37 +02:00
2022-05-19 17:47:49 +02:00
_Bootstrap = tcpRpcClient.GetMain<IBootstrap>();
2023-01-25 01:48:54 +01:00
ServerData = await _GetServerData(_Bootstrap);
2022-05-12 23:08:37 +02:00
2022-05-19 17:47:49 +02:00
Session = await _Authenticate(connectionData).ConfigureAwait(false);
ConnectionData = connectionData;
2022-05-16 22:41:29 +02:00
2022-05-19 17:47:49 +02:00
_TcpRpcClient = tcpRpcClient;
tcpRpcClient.ConnectionStateChanged += OnTcpRpcConnectionChanged;
2023-01-25 01:48:54 +01:00
ConnectionStatusChanged?.Invoke(this, FabAccessAPI.ConnectionStatusChanged.Connected);
2022-05-19 17:47:49 +02:00
Log.Info("API connected");
}
catch (System.Exception ex)
{
2023-01-25 01:48:54 +01:00
Log.Warn(ex, "API connect failed");
2022-05-19 17:47:49 +02:00
throw ex;
}
2022-05-11 15:02:17 +02:00
}
2022-05-19 17:47:49 +02:00
finally
2022-05-11 15:02:17 +02:00
{
2022-05-19 17:47:49 +02:00
_ConnectSemaphore.Release();
2022-05-11 15:02:17 +02:00
}
2022-05-10 13:35:23 +02:00
}
2023-01-25 01:48:54 +01:00
/// <summary>
/// Disconnect from a server
/// </summary>
/// <exception cref="InvalidOperationException"> When API-Service is not connected or trying to connect </exception>
2022-05-11 15:02:17 +02:00
public Task Disconnect()
2022-05-10 13:35:23 +02:00
{
2022-05-11 15:02:17 +02:00
if (IsConnected)
{
2022-05-12 23:08:37 +02:00
_TcpRpcClient.Dispose();
2022-05-11 15:02:17 +02:00
}
2023-01-25 01:48:54 +01:00
2022-05-12 23:08:37 +02:00
_Bootstrap = null;
_TcpRpcClient = null;
2023-01-25 01:48:54 +01:00
Session = null;
2022-05-11 15:02:17 +02:00
ConnectionData = null;
2023-01-25 01:48:54 +01:00
ServerData = null;
2022-05-11 15:02:17 +02:00
2023-01-25 01:48:54 +01:00
ConnectionStatusChanged?.Invoke(this, FabAccessAPI.ConnectionStatusChanged.Disconnected);
2022-05-12 23:08:37 +02:00
2022-05-16 22:41:29 +02:00
Log.Info("API disconnected");
2022-05-11 15:02:17 +02:00
return Task.CompletedTask;
2022-05-10 13:35:23 +02:00
}
2023-01-25 01:48:54 +01:00
/// <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)
2022-05-10 13:35:23 +02:00
{
2023-01-28 01:15:43 +01:00
if (tcpRpcClient == null)
2022-05-11 15:02:17 +02:00
{
2023-01-28 01:15:43 +01:00
tcpRpcClient = new TcpRpcClient();
}
2022-05-11 15:02:17 +02:00
2023-01-28 01:15:43 +01:00
await _ConnectAsync(tcpRpcClient, connectionData).ConfigureAwait(false);
IBootstrap bootstrap = tcpRpcClient.GetMain<IBootstrap>();
2022-05-12 23:08:37 +02:00
2023-01-28 01:15:43 +01:00
ServerData serverData = await _GetServerData(bootstrap).ConfigureAwait(false);
2022-05-12 23:08:37 +02:00
2023-01-28 01:15:43 +01:00
tcpRpcClient.Dispose();
2022-05-12 23:08:37 +02:00
2023-01-28 01:15:43 +01:00
return serverData;
2022-05-11 15:02:17 +02:00
}
2023-01-25 01:48:54 +01:00
/// <summary>
/// Public Wrapper to run HeartbeatAsync
/// </summary>
public void Heartbeat(object state)
{
_ = HeartbeatAsync();
}
2022-05-11 15:02:17 +02:00
#endregion
#region Private Methods
2023-01-25 01:48:54 +01:00
private async Task HeartbeatAsync()
{
if(!IsConnected && CanConnect)
{
2023-02-26 19:50:58 +01:00
try
{
await Connect(ConnectionData).ConfigureAwait(false);
}
catch(AuthenticationException)
{
await Disconnect().ConfigureAwait(false);
}
2023-01-25 01:48:54 +01:00
}
}
2022-05-12 23:08:37 +02:00
/// <summary>
/// Validate Certificate
/// TODO: Do some validation
/// </summary>
2023-01-25 01:48:54 +01:00
private bool _RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
2022-05-11 15:02:17 +02:00
{
// TODO Cert Check
return true;
}
2023-01-31 14:14:33 +01:00
/// <summary>
/// Injects SSL as Midlayer in TCPRPCConnection
/// </summary>
/// <exception cref="ConnectionException"></exception>
private Stream InjectSSL(Stream tcpstream)
{
SslStream sslStream = new SslStream(tcpstream, false, new RemoteCertificateValidationCallback(_RemoteCertificateValidationCallback));
try
{
2023-02-02 02:35:47 +01:00
sslStream.ReadTimeout = 5000;
2023-01-31 14:14:33 +01:00
sslStream.AuthenticateAsClient("bffhd");
sslStream.ReadTimeout = -1;
return sslStream;
}
catch (System.Security.Authentication.AuthenticationException exception)
{
sslStream.Close();
Log.Warn(exception);
throw new ConnectionException("TLS failed", exception);
}
catch(IOException exception)
{
sslStream.Close();
Log.Warn(exception);
throw new ConnectionException("TLS failed", new Exceptions.TimeoutException("TLS timeout", exception));
}
}
2022-05-11 15:02:17 +02:00
/// <summary>
2023-01-25 01:48:54 +01:00
/// Connect async to a server with ConnectionData
2022-05-11 15:02:17 +02:00
/// </summary>
2023-01-25 01:48:54 +01:00
/// <exception cref="ConnectionException">Based on RPC Exception</exception>
private async Task _ConnectAsync(TcpRpcClient tcprpcClient, ConnectionData connectionData)
2022-05-11 15:02:17 +02:00
{
2023-01-31 14:14:33 +01:00
tcprpcClient.InjectMidlayer(InjectSSL);
2022-05-11 15:02:17 +02:00
try
{
2023-02-02 02:35:47 +01:00
Task timeoutTask = Task.Delay(5000);
2023-01-25 01:48:54 +01:00
tcprpcClient.Connect(connectionData.Host.Host, connectionData.Host.Port);
await await Task.WhenAny(tcprpcClient.WhenConnected, timeoutTask);
if (timeoutTask.IsCompleted)
2022-06-02 13:15:03 +02:00
{
2023-01-25 01:48:54 +01:00
Exceptions.TimeoutException timeoutException = new Exceptions.TimeoutException();
Log.Warn(timeoutException);
throw new ConnectionException("Connection timeout", timeoutException);
2022-06-02 13:15:03 +02:00
}
2022-05-11 15:02:17 +02:00
}
catch (RpcException exception) when (string.Equals(exception.Message, "TcpRpcClient is unable to connect", StringComparison.Ordinal))
{
2023-01-25 01:48:54 +01:00
Log.Warn(exception);
throw new ConnectionException("RPC Connecting failed", exception);
2022-05-11 15:02:17 +02:00
}
}
/// <summary>
/// Authenticate connection with ConnectionData
/// </summary>
2023-01-25 01:48:54 +01:00
/// <param name="connectionData"> Data to establish a connection to a server </param>
/// <exception cref="AuthenticationException"></exception>
2022-05-12 23:08:37 +02:00
private async Task<Session> _Authenticate(ConnectionData connectionData)
2022-05-11 15:02:17 +02:00
{
2023-01-25 01:48:54 +01:00
IAuthentication? authentication = await _Bootstrap.CreateSession(SASLMechanism.ToString(connectionData.Mechanism)).ConfigureAwait(false);
2022-05-12 23:08:37 +02:00
2023-01-25 01:48:54 +01:00
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;
}
2022-05-12 23:08:37 +02:00
}
/// <summary>
2023-01-25 01:48:54 +01:00
/// Authenticate with SASL
2022-05-12 23:08:37 +02:00
/// </summary>
/// <exception cref="BadMechanismException"></exception>
/// <exception cref="InvalidCredentialsException"></exception>
/// <exception cref="AuthenticationFailedException"></exception>
2022-05-16 16:07:33 +02:00
private async Task<Session> _SASLAuthenticate(IAuthentication authentication, string mech, Dictionary<string, object> properties)
2022-05-12 23:08:37 +02:00
{
SaslMechanism? saslMechanism = SaslFactory.Create(mech);
foreach (KeyValuePair<string, object> entry in properties)
{
saslMechanism.Properties.Add(entry.Key, entry.Value);
}
byte[] data = new byte[0];
if (saslMechanism.HasInitial)
{
data = saslMechanism.GetResponse(new byte[0]);
}
Response? response = await authentication.Step(data);
while (!saslMechanism.IsCompleted)
{
2023-01-25 01:48:54 +01:00
if (response.Failed != null)
2022-05-12 23:08:37 +02:00
{
break;
}
2023-01-25 01:48:54 +01:00
if (response.Challenge != null)
2022-05-12 23:08:37 +02:00
{
byte[]? additional = saslMechanism.GetResponse(response.Challenge.ToArray());
response = await authentication.Step(additional);
}
else
{
throw new AuthenticationFailedException();
}
}
if (response.Successful != null)
{
return response.Successful.Session;
}
else if (response.Failed != null)
{
switch (response.Failed.Code)
{
case Response.Error.badMechanism:
throw new BadMechanismException();
case Response.Error.invalidCredentials:
throw new InvalidCredentialsException();
case Response.Error.aborted:
case Response.Error.failed:
default:
throw new AuthenticationFailedException(response.Failed.AdditionalData.ToArray());
}
}
else
{
throw new AuthenticationFailedException();
}
2022-05-10 13:35:23 +02:00
}
2023-01-25 01:48:54 +01:00
/// <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;
}
2022-05-10 13:35:23 +02:00
#endregion
}
}