using Capnp.Rpc;
using FabAccessAPI.Exceptions;
using FabAccessAPI.Exceptions.SASL;
using FabAccessAPI.Schema;
using NLog;
using S22.Sasl;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
namespace FabAccessAPI
{
public class API : IAPI
{
#region Logger
private static readonly Logger Log = LogManager.GetCurrentClassLogger();
#endregion
#region Private Members
///
/// Internal client to connect to a server with TCP and RPC
///
private TcpRpcClient _TcpRpcClient;
///
/// Private ConnectionData
///
private ConnectionData _ConnectionData;
///
/// Private ServerData
///
private ServerData _ServerData;
///
/// Private Session
///
private Session _Session;
///
/// Private Bootstrap
///
private IBootstrap _Bootstrap;
///
/// Timer to check connection status
///
private readonly Timer _ConnectionHeatbeat;
///
/// Semaphore to connect only once
///
private static readonly SemaphoreSlim _ConnectSemaphore = new SemaphoreSlim(1, 1);
#endregion
#region Constructors
public API()
{
_ConnectionHeatbeat = new Timer(Heartbeat, null, 1000, 1000);
}
#endregion
#region Members
///
/// State of the conneciton, can the API-Service connect to a server
///
public bool CanConnect
{
get
{
return _ConnectionData != null;
}
}
///
/// State of the conneciton, is the API-Service connecting to a server
///
public bool IsConnecting
{
get
{
return _TcpRpcClient != null && _ConnectionData != null;
}
}
///
/// State of the conneciton, is the API-Service connected to a server
///
public bool IsConnected
{
get
{
return _TcpRpcClient != null && _TcpRpcClient.State == ConnectionState.Active;
}
}
///
/// Information about the connection
///
/// When API-Service is not connected or trying to connected to a server
public ConnectionData ConnectionData
{
get
{
if(_ConnectionData == null || !IsConnecting)
{
throw new InvalidOperationException();
}
else
{
return _ConnectionData;
}
}
private set
{
_ConnectionData = value;
}
}
///
/// Information about the server
/// Is only avalible if the API-Service is connected
///
/// When API-Service is not connected
public ServerData ServerData
{
get
{
if (_ServerData == null || !IsConnected)
{
throw new InvalidOperationException();
}
else
{
return _ServerData;
}
}
private set
{
_ServerData = value;
}
}
#endregion
#region Events
///
/// Event on changes in connection status
///
public event EventHandler ConnectionStatusChanged;
///
/// Unbind all handlers from EventHandler
///
public void UnbindEventHandler()
{
if (ConnectionStatusChanged != null)
{
Log.Trace("Eventhandlers unbinded");
foreach (Delegate d in ConnectionStatusChanged.GetInvocationList())
{
ConnectionStatusChanged -= (EventHandler)d;
}
}
}
///
/// Eventhandler for TcpRpcConnectionChanged
/// Track connection loss and publish i in 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, FabAccessAPI.ConnectionStatusChanged.ConnectionLoss);
}
}
#endregion
#region Session
///
/// Get session after connection
///
/// When API-Service is not connected
public Session Session
{
get
{
if (_Session == null || !IsConnected)
{
throw new InvalidOperationException();
}
else
{
return _Session;
}
}
private set
{
_Session = value;
}
}
#endregion
#region Methods
///
/// Connect to server with ConnectionData
/// If connection lost, the API-Server will try to reconnect
///
/// Data to establish a connection to a server
/// When API-Service can not connect to a server
/// When API-Service can connect to a server but can not authenticate
/// When API-Service is allready connected
public async Task Connect(ConnectionData connectionData, TcpRpcClient tcpRpcClient = null)
{
await _ConnectSemaphore.WaitAsync();
try
{
if (IsConnected)
{
Log.Warn("API already connected");
throw new InvalidOperationException();
}
if (tcpRpcClient == null)
{
tcpRpcClient = new TcpRpcClient();
}
try
{
await _ConnectAsync(tcpRpcClient, connectionData).ConfigureAwait(false);
_Bootstrap = tcpRpcClient.GetMain();
ServerData = await _GetServerData(_Bootstrap);
Session = await _Authenticate(connectionData).ConfigureAwait(false);
ConnectionData = connectionData;
_TcpRpcClient = tcpRpcClient;
tcpRpcClient.ConnectionStateChanged += OnTcpRpcConnectionChanged;
ConnectionStatusChanged?.Invoke(this, FabAccessAPI.ConnectionStatusChanged.Connected);
Log.Info("API connected");
}
catch (System.Exception ex)
{
Log.Warn(ex, "API connect failed");
throw ex;
}
}
finally
{
_ConnectSemaphore.Release();
}
}
///
/// Disconnect from a server
///
/// When API-Service is not connected or trying to connect
public Task Disconnect()
{
if (IsConnected)
{
_TcpRpcClient.Dispose();
}
_Bootstrap = null;
_TcpRpcClient = null;
Session = null;
ConnectionData = null;
ServerData = null;
ConnectionStatusChanged?.Invoke(this, FabAccessAPI.ConnectionStatusChanged.Disconnected);
Log.Info("API disconnected");
return Task.CompletedTask;
}
///
/// Try to connect to a server and get ServerData
/// The connection is not maintained
///
/// When API-Service can not connect to a server
public async Task TryToConnect(ConnectionData connectionData, TcpRpcClient tcpRpcClient = null)
{
if (tcpRpcClient == null)
{
tcpRpcClient = new TcpRpcClient();
}
await _ConnectAsync(tcpRpcClient, connectionData).ConfigureAwait(false);
IBootstrap bootstrap = tcpRpcClient.GetMain();
ServerData serverData = await _GetServerData(bootstrap).ConfigureAwait(false);
tcpRpcClient.Dispose();
return serverData;
}
///
/// Public Wrapper to run HeartbeatAsync
///
public void Heartbeat(object state)
{
_ = _HeartbeatAsync();
}
#endregion
#region Private Methods
private async Task _HeartbeatAsync()
{
if(!IsConnected && CanConnect)
{
try
{
await Connect(ConnectionData).ConfigureAwait(false);
}
catch(AuthenticationException)
{
await Disconnect().ConfigureAwait(false);
}
}
}
///
/// Validate Certificate
/// TODO: Do some validation
///
private bool _RemoteCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// TODO Cert Check
return true;
}
///
/// Injects SSL as Midlayer in TCPRPCConnection
///
///
private Stream _InjectSSL(Stream tcpstream)
{
SslStream sslStream = new SslStream(tcpstream, false, new RemoteCertificateValidationCallback(_RemoteCertificateValidationCallback));
try
{
sslStream.ReadTimeout = 5000;
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));
}
}
///
/// Connect async to a server with ConnectionData
///
/// Based on RPC Exception
private async Task _ConnectAsync(TcpRpcClient tcprpcClient, ConnectionData connectionData)
{
tcprpcClient.InjectMidlayer(_InjectSSL);
try
{
Task timeoutTask = Task.Delay(5000);
tcprpcClient.Connect(connectionData.Host.Host, connectionData.Host.Port);
await await Task.WhenAny(tcprpcClient.WhenConnected, timeoutTask);
if (timeoutTask.IsCompleted)
{
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))
{
Log.Warn(exception);
throw new ConnectionException("RPC Connecting failed", exception);
}
}
///
/// Authenticate connection with ConnectionData
///
/// Data to establish a connection to a server
///
private async Task _Authenticate(ConnectionData connectionData)
{
throw new NotImplementedException();
// IAuthentication? authentication = await _Bootstrap.CreateSession(SASLMechanism.ToString(connectionData.Mechanism)).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;
// }
}
///
/// Authenticate with SASL
///
///
///
///
// private async Task _SASLAuthenticate(IAuthentication authentication, string mech, Dictionary properties)
// {
// SaslMechanism? saslMechanism = SaslFactory.Create(mech);
// foreach (KeyValuePair 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)
// {
// if (response != null)
// {
// break;
// }
// if (response.Challenge != null)
// {
// 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.Error != null)
// {
// switch (response.Error.Reason)
// {
// case Response.Reason.badMechanism:
// throw new BadMechanismException();
// case Response.Reason.invalidCredentials:
// throw new InvalidCredentialsException();
// case Response.Reason.aborted:
// case Response.Reason.failed:
// default:
// throw new AuthenticationFailedException();
// // TODO throw new AuthenticationFailedException(response.Error.AdditionalData.ToArray());
// }
// }
// else
// {
// throw new AuthenticationFailedException();
// }
// }
///
/// Get ServerData from server with tcprpcconnection
///
private async Task _GetServerData(IBootstrap bootstrap)
{
var release = await bootstrap.GetServerRelease().ConfigureAwait(false);
var info = await bootstrap.GetServerInfo().ConfigureAwait(false);
ServerData serverData = new ServerData()
{
APIVersion = await bootstrap.GetAPIVersion().ConfigureAwait(false),
AuthSupported = await bootstrap.Mechanisms().ConfigureAwait(false),
ServerName = release.Item1,
ServerRelease = release.Item2,
SpaceName = info.Item1,
InstanceURL = info.Item2
};
return serverData;
}
#endregion
}
}