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 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) { 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.Failed != 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.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(); } } /// /// Get ServerData from server with tcprpcconnection /// private async Task _GetServerData(IBootstrap bootstrap) { ServerData serverData = new ServerData() { APIVersion = await bootstrap.GetAPIVersion().ConfigureAwait(false), Mechanisms = new List(await bootstrap.Mechanisms().ConfigureAwait(false)), ServerName = (await bootstrap.GetServerRelease().ConfigureAwait(false)).Item1, ServerRelease = (await bootstrap.GetServerRelease().ConfigureAwait(false)).Item2, }; return serverData; } #endregion } }