//****************************************************************************************************** // TlsServer.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 07/12/2012 - Stephen C. Wills // Generated original version of source code. // 12/13/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using GSF.Configuration; using GSF.IO; using GSF.Net.Security; using GSF.Threading; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.Text; using System.Threading; #if MONO #pragma warning disable 649 #endif namespace GSF.Communication { /// /// Represents a TCP-based communication server with SSL authentication and encryption. /// public class TlsServer : ServerBase { #region [ Members ] // Nested Types /// /// Represents a socket that has been wrapped /// in an for encryption. /// public sealed class TlsSocket : IDisposable { /// /// Gets the connected to the remote host. /// public Socket Socket; /// /// Gets the stream through which data is passed when /// sending to or receiving from the remote host. /// public SslStream SslStream; /// /// The end point of the remote client connecting to this server. /// public IPEndPoint RemoteEndPoint; /// /// Performs application-defined tasks associated with /// freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Socket?.Dispose(); SslStream?.Dispose(); } } private class TlsClientInfo { public TransportProvider Client; public ICancellationToken TimeoutToken; public int Sending; public object SendLock; public ConcurrentQueue SendQueue; public ShortSynchronizedOperation DumpPayloadsOperation; public NegotiateStream NegotiateStream; public WindowsPrincipal ClientPrincipal; } private class TlsServerPayload { // Per payload state public byte[] Data; public int Offset; public int Length; public ManualResetEventSlim WaitHandle; // Per client state public TlsClientInfo ClientInfo; } // Constants /// /// Specifies the default value for the property. /// public readonly string DefaultTrustedCertificatesPath = FilePath.GetAbsolutePath(@"Certs\Remotes"); /// /// Specifies the default value for the property. /// public const bool DefaultPayloadAware = false; /// /// Specifies the default value for the property. /// public const bool DefaultIntegratedSecurity = false; /// /// Specifies the default value for the property. /// public const bool DefaultIgnoreInvalidCredentials = false; /// /// Specifies the default value for the property. /// public const bool DefaultAllowDualStackSocket = true; /// /// Specifies the default value for the property. /// public const int DefaultMaxSendQueueSize = 500000; /// /// Specifies the default value for the property. /// public const bool DefaultNoDelay = false; /// /// Specifies the default value for the property. /// public const string DefaultConfigurationString = "Port=8888"; // Fields private readonly SimpleCertificateChecker m_defaultCertificateChecker; private ICertificateChecker m_certificateChecker; private string m_certificateFile; private SslProtocols m_enabledSslProtocols; private byte[] m_payloadMarker; private EndianOrder m_payloadEndianOrder; private IPStack m_ipStack; private SocketAsyncEventArgs m_acceptArgs; private readonly ConcurrentDictionary m_clientInfoLookup; private Dictionary m_configData; private readonly EventHandler m_acceptHandler; #endregion #region [ Constructors ] /// /// Initializes a new instance of the class. /// public TlsServer() : this(DefaultConfigurationString) { } /// /// Initializes a new instance of the class. /// /// Config string of the . See for format. public TlsServer(string configString) : base(TransportProtocol.Tcp, configString) { m_defaultCertificateChecker = new SimpleCertificateChecker(); LocalCertificateSelectionCallback = DefaultLocalCertificateSelectionCallback; m_enabledSslProtocols = SslProtocols.Tls12; CheckCertificateRevocation = true; TrustedCertificatesPath = DefaultTrustedCertificatesPath; PayloadAware = DefaultPayloadAware; m_payloadMarker = Payload.DefaultMarker; m_payloadEndianOrder = EndianOrder.LittleEndian; IntegratedSecurity = DefaultIntegratedSecurity; IgnoreInvalidCredentials = DefaultIgnoreInvalidCredentials; AllowDualStackSocket = DefaultAllowDualStackSocket; MaxSendQueueSize = DefaultMaxSendQueueSize; NoDelay = DefaultNoDelay; m_clientInfoLookup = new ConcurrentDictionary(); m_acceptHandler = (sender, args) => ProcessAccept(args); } /// /// Initializes a new instance of the class. /// /// object that contains the . public TlsServer(IContainer container) : this() => container?.Add(this); #endregion #region [ Properties ] /// /// Gets or sets a boolean value that indicates whether the payload boundaries are to be preserved during transmission. /// [Category("Data")] [DefaultValue(DefaultPayloadAware)] [Description("Indicates whether the payload boundaries are to be preserved during transmission.")] public bool PayloadAware { get; set; } /// /// Gets or sets the byte sequence used to mark the beginning of a payload in a transmission. /// /// /// Setting property to null will create a zero-length payload marker. /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public byte[] PayloadMarker { get => m_payloadMarker; set => m_payloadMarker = value ?? Array.Empty(); } /// /// Gets or sets the endian order to apply for encoding and decoding payload size in a transmission. /// /// /// Setting property to null will force use of little-endian encoding. /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public EndianOrder PayloadEndianOrder { get => m_payloadEndianOrder; set => m_payloadEndianOrder = value ?? EndianOrder.LittleEndian; } /// /// Gets or sets a boolean value that indicates whether the client Windows account credentials are used for authentication. /// /// /// This option is ignored under Mono deployments. /// [Category("Security")] [DefaultValue(DefaultIntegratedSecurity)] [Description("Indicates whether the client Windows account credentials are used for authentication.")] public bool IntegratedSecurity { get; set; } /// /// Gets or sets a boolean value that indicates whether the server /// should ignore errors when the client's credentials are invalid. /// /// /// This property should only be set to true if there is an alternative by which /// to authenticate the client when integrated security fails. When this is set /// to true, if the client's credentials are invalid, the /// method will return true for that client, but the principal will still be null. /// [Category("Security")] [DefaultValue(DefaultIgnoreInvalidCredentials)] [Description("Indicates whether the client Windows account credentials are validated during authentication.")] public bool IgnoreInvalidCredentials { get; set; } /// /// Gets or sets a boolean value that determines if dual-mode socket is allowed when endpoint address is IPv6. /// [Category("Settings")] [DefaultValue(DefaultAllowDualStackSocket)] [Description("Determines if dual-mode socket is allowed when endpoint address is IPv6.")] public bool AllowDualStackSocket { get; set; } /// /// Gets or sets the maximum size for the send queue before payloads are dumped from the queue. /// [Category("Settings")] [DefaultValue(DefaultMaxSendQueueSize)] [Description("The maximum size for the send queue before payloads are dumped from the queue.")] public int MaxSendQueueSize { get; set; } /// /// Gets or sets a boolean value that determines if small packets are delivered to the remote host without delay. /// [Category("Settings")] [DefaultValue(DefaultNoDelay)] [Description("Determines if small packets are delivered to the remote host without delay.")] public bool NoDelay { get; set; } /// /// Gets the object for the . /// [Browsable(false)] public Socket Server { get; private set; } /// /// Gets or sets the certificate checker used to validate remote certificates. /// /// /// The certificate checker will only be used to validate certificates if /// the is set to null. /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ICertificateChecker CertificateChecker { get => m_certificateChecker ?? m_defaultCertificateChecker; set => m_certificateChecker = value; } /// /// Gets or sets the callback used to validate remote certificates. /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; } /// /// Gets or sets the callback used to select local certificates. /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public LocalCertificateSelectionCallback LocalCertificateSelectionCallback { get; set; } /// /// Gets or sets the path to the certificate used for authentication. /// [Category("Settings")] [DefaultValue(null)] [Description("Path to the local certificate used by this server for authentication.")] public string CertificateFile { get => m_certificateFile; set { if (string.IsNullOrWhiteSpace(value)) { m_certificateFile = null; Certificate = null; } else { m_certificateFile = FilePath.GetAbsolutePath(value); if (File.Exists(m_certificateFile)) Certificate = new X509Certificate2(m_certificateFile); } } } /// /// Gets or sets the certificate used to identify this server. /// [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public X509Certificate Certificate { get; set; } /// /// Gets or sets a set of flags which determine the enabled . /// /// Failed to write event log entry for security warning about use of less secure TLS/SSL protocols. [Category("Settings")] [DefaultValue(SslProtocols.Tls12)] [Description("The set of SSL protocols that are enabled for this server.")] public SslProtocols EnabledSslProtocols { get => m_enabledSslProtocols; set { m_enabledSslProtocols = value; // As of July 2014, Tls12 is the latest and most secure TLS protocol - all other protocols // represent a security risk when used, as such we log a security message when any of the // older protocols are being used. if (m_enabledSslProtocols >= SslProtocols.Tls12) return; try { string applicationName; // Get application name try { // Attempt to retrieve application name as defined in common security settings - this name // is typically preconfigured as the desired event source for event log entries ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings["SecurityProvider"]; applicationName = settings["ApplicationName"].Value; } catch { applicationName = null; } // Fall back on running executable name if (string.IsNullOrWhiteSpace(applicationName)) applicationName = FilePath.GetFileNameWithoutExtension(AppDomain.CurrentDomain.FriendlyName); string message = $"One or more less secure TLS/SSL protocols \"{m_enabledSslProtocols}\" are being used by an instance of the TlsServer in {applicationName}"; EventLog.WriteEntry(applicationName, message, EventLogEntryType.Warning, 1); } catch (Exception ex) { throw new SecurityException($"Failed to write event log entry for security warning about use of less secure TLS/SSL protocols \"{m_enabledSslProtocols}\": {ex.Message}", ex); } } } /// /// Gets or sets a flag that determines whether a client certificate is required during authentication. /// [Category("Settings")] [DefaultValue(false)] [Description("True if the client certificate is required during authentication, otherwise False.")] public bool RequireClientCertificate { get; set; } /// /// Gets or sets a boolean value that determines whether the certificate revocation list is checked during authentication. /// [Category("Settings")] [DefaultValue(true)] [Description("True if the certificate revocation list is to be checked during authentication, otherwise False.")] public bool CheckCertificateRevocation { get; set; } /// /// Gets or sets the path to the directory containing the trusted certificates. /// [Category("Settings")] [DefaultValue("Trusted Certificates")] [Description("Path to the directory containing the trusted remote certificates.")] public string TrustedCertificatesPath { get; set; } /// /// Gets or sets the set of valid policy errors when validating remote certificates. /// [Category("Settings")] [DefaultValue(SslPolicyErrors.None)] [Description("Set of valid policy errors when validating remote certificates.")] public SslPolicyErrors ValidPolicyErrors { get => m_defaultCertificateChecker.ValidPolicyErrors; set => m_defaultCertificateChecker.ValidPolicyErrors = value; } /// /// Gets or sets the set of valid chain flags used when validating remote certificates. /// [Category("Settings")] [DefaultValue(X509ChainStatusFlags.NoError)] [Description("Set of valid chain flags used when validating remote certificates.")] public X509ChainStatusFlags ValidChainFlags { get => m_defaultCertificateChecker.ValidChainFlags; set => m_defaultCertificateChecker.ValidChainFlags = value; } /// /// Gets the descriptive status of the server. /// public override string Status { get { StringBuilder statusBuilder = new StringBuilder(base.Status); int count = 0; foreach (ConcurrentQueue sendQueue in m_clientInfoLookup.Values.Select(clientInfo => clientInfo.SendQueue)) { statusBuilder.AppendFormat(" Queued payloads: {0} for client {1}", sendQueue.Count, ++count); statusBuilder.AppendLine(); } return statusBuilder.ToString(); } } #endregion #region [ Methods ] /// /// Reads a number of bytes from the current received data buffer and writes those bytes into a byte array at the specified offset. /// /// ID of the client from which data buffer should be read. /// Destination buffer used to hold copied bytes. /// 0-based starting index into destination to begin writing data. /// The number of bytes to read from current received data buffer and write into . /// The number of bytes read. /// /// This function should only be called from within the event handler. Calling this method /// outside this event will have unexpected results. /// /// /// No received data buffer has been defined to read -or- /// Specified does not exist, cannot read buffer. /// /// is null. /// /// or is less than 0 -or- /// and will exceed length. /// public override int Read(Guid clientID, byte[] buffer, int startIndex, int length) { buffer.ValidateParameters(startIndex, length); if (!m_clientInfoLookup.TryGetValue(clientID, out TlsClientInfo clientInfo)) throw new InvalidOperationException("Specified client ID does not exist, cannot read buffer."); TransportProvider tlsClient = clientInfo.Client; if (tlsClient.ReceiveBuffer == null) throw new InvalidOperationException("No received data buffer has been defined to read."); int readIndex = ReadIndicies[clientID]; int sourceLength = tlsClient.BytesReceived - readIndex; int readBytes = length > sourceLength ? sourceLength : length; Buffer.BlockCopy(tlsClient.ReceiveBuffer, readIndex, buffer, startIndex, readBytes); // Update read index for next call readIndex += readBytes; if (readIndex >= tlsClient.BytesReceived) readIndex = 0; ReadIndicies[clientID] = readIndex; return readBytes; } /// /// Saves settings to the config file if the property is set to true. /// public override void SaveSettings() { base.SaveSettings(); if (!PersistSettings) return; // Save settings under the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory]; settings["EnabledSslProtocols", true].Update(m_enabledSslProtocols); settings["RequireClientCertificate", true].Update(RequireClientCertificate); settings["CheckCertificateRevocation", true].Update(CheckCertificateRevocation); settings["CertificateFile", true].Update(m_certificateFile); settings["TrustedCertificatesPath", true].Update(TrustedCertificatesPath); settings["ValidPolicyErrors", true].Update(ValidPolicyErrors); settings["ValidChainFlags", true].Update(ValidChainFlags); settings["PayloadAware", true].Update(PayloadAware); settings["IntegratedSecurity", true].Update(IntegratedSecurity); settings["AllowDualStackSocket", true].Update(AllowDualStackSocket); settings["MaxSendQueueSize", true].Update(MaxSendQueueSize); settings["NoDelay", true].Update(NoDelay); config.Save(); } /// /// Loads saved settings from the config file if the property is set to true. /// public override void LoadSettings() { base.LoadSettings(); if (PersistSettings) { // Load settings from the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory]; settings.Add("EnabledSslProtocols", m_enabledSslProtocols, "The set of SSL protocols that are enabled for this server."); settings.Add("RequireClientCertificate", RequireClientCertificate, "True if the client certificate is required during authentication, otherwise False."); settings.Add("CheckCertificateRevocation", CheckCertificateRevocation, "True if the certificate revocation list is to be checked during authentication, otherwise False."); settings.Add("CertificateFile", m_certificateFile, "Path to the local certificate used by this server for authentication."); settings.Add("TrustedCertificatesPath", TrustedCertificatesPath, "Path to the directory containing the trusted remote certificates."); settings.Add("ValidPolicyErrors", ValidPolicyErrors, "Set of valid policy errors when validating remote certificates."); settings.Add("ValidChainFlags", ValidChainFlags, "Set of valid chain flags used when validating remote certificates."); settings.Add("PayloadAware", PayloadAware, "True if payload boundaries are to be preserved during transmission, otherwise False."); settings.Add("IntegratedSecurity", IntegratedSecurity, "True if the client Windows account credentials are used for authentication, otherwise False."); settings.Add("AllowDualStackSocket", AllowDualStackSocket, "True if dual-mode socket is allowed when IP address is IPv6, otherwise False."); settings.Add("MaxSendQueueSize", MaxSendQueueSize, "The maximum size of the send queue before payloads are dumped from the queue."); settings.Add("NoDelay", NoDelay, "True to disable Nagle so that small packets are delivered to the remote host without delay, otherwise False."); try { // Attempt to set desired transport security protocols EnabledSslProtocols = settings["EnabledSslProtocols"].ValueAs(m_enabledSslProtocols); } catch (SecurityException ex) { // Security exception can occur when user forces use of older TLS protocol through configuration but event log warning entry cannot be written OnClientConnectingException(new SecurityException($"Transport layer security protocols assigned as configured: \"{EnabledSslProtocols}\", however, event log entry for security exception could not be written: {ex.Message}", ex)); } RequireClientCertificate = settings["RequireClientCertificate"].ValueAs(RequireClientCertificate); CheckCertificateRevocation = settings["CheckCertificateRevocation"].ValueAs(CheckCertificateRevocation); CertificateFile = settings["CertificateFile"].ValueAs(m_certificateFile); TrustedCertificatesPath = settings["TrustedCertificatesPath"].ValueAs(TrustedCertificatesPath); ValidPolicyErrors = settings["ValidPolicyErrors"].ValueAs(ValidPolicyErrors); ValidChainFlags = settings["ValidChainFlags"].ValueAs(ValidChainFlags); PayloadAware = settings["PayloadAware"].ValueAs(PayloadAware); IntegratedSecurity = settings["IntegratedSecurity"].ValueAs(IntegratedSecurity); AllowDualStackSocket = settings["AllowDualStackSocket"].ValueAs(AllowDualStackSocket); MaxSendQueueSize = settings["MaxSendQueueSize"].ValueAs(MaxSendQueueSize); NoDelay = settings["NoDelay"].ValueAs(NoDelay); } if (!FilePath.InApplicationPath(TrustedCertificatesPath)) OnClientConnectingException(new SecurityException($"Trusted certificates path \"{TrustedCertificatesPath}\" is not in application path")); } /// /// Stops the synchronously and disconnects all connected clients. /// public override void Stop() { SocketAsyncEventArgs acceptArgs = m_acceptArgs; m_acceptArgs = null; if (CurrentState != ServerState.Running) return; DisconnectAll(); // Disconnection all clients. Server.Close(); // Stop accepting new connections. // Clean up accept args. acceptArgs?.Dispose(); OnServerStopped(); } /// /// Starts the synchronously and begins accepting client connections asynchronously. /// /// Attempt is made to the when it is running. public override void Start() { if (CurrentState != ServerState.NotRunning) throw new InvalidOperationException("Server is currently running"); // Initialize if unitialized. if (!Initialized) Initialize(); // Overwrite config file if integrated security exists in connection string. if (m_configData.TryGetValue("integratedSecurity", out string integratedSecuritySetting)) IntegratedSecurity = integratedSecuritySetting.ParseBoolean(); #if MONO // Force integrated security to be False under Mono since it's not supported m_integratedSecurity = false; #endif // Overwrite config file if no delay exists in connection string. if (m_configData.TryGetValue("noDelay", out string noDelaySetting)) NoDelay = noDelaySetting.ParseBoolean(); // Bind server socket to local end-point and listen. Server = Transport.CreateSocket(m_configData["interface"], int.Parse(m_configData["port"]), ProtocolType.Tcp, m_ipStack, AllowDualStackSocket); Server.NoDelay = NoDelay; Server.Listen(1); // Begin accepting incoming connection asynchronously. m_acceptArgs = FastObjectFactory.CreateObjectFunction(); m_acceptArgs.AcceptSocket = null; m_acceptArgs.SetBuffer(null, 0, 0); m_acceptArgs.SocketFlags = SocketFlags.None; m_acceptArgs.Completed += m_acceptHandler; if (!Server.AcceptAsync(m_acceptArgs)) ThreadPool.QueueUserWorkItem(state => ProcessAccept((SocketAsyncEventArgs)state), m_acceptArgs); // Notify that the server has been started successfully. OnServerStarted(); } /// /// Disconnects the specified connected client. /// /// ID of the client to be disconnected. /// Client does not exist for the specified . public override void DisconnectOne(Guid clientID) { if (!TryGetClient(clientID, out TransportProvider tlsClient) || tlsClient is null) return; try { if (tlsClient.Provider != null && tlsClient.Provider.Socket.Connected) tlsClient.Provider.Socket.Disconnect(false); OnClientDisconnected(clientID); tlsClient.Reset(); } catch (Exception ex) { OnSendClientDataException(clientID, new InvalidOperationException($"Client disconnection exception: {ex.Message}", ex)); } } /// /// Gets the object associated with the specified client ID. /// /// ID of the client. /// The TLS client. /// An object. /// Client does not exist for the specified . public bool TryGetClient(Guid clientID, out TransportProvider tlsClient) { bool clientExists = m_clientInfoLookup.TryGetValue(clientID, out TlsClientInfo clientInfo); tlsClient = clientExists ? clientInfo.Client : null; return clientExists; } /// /// Gets the object associated with the specified client ID. /// /// ID of the client. /// The principal of the client. /// A object. public bool TryGetClientPrincipal(Guid clientID, out WindowsPrincipal clientPrincipal) { bool clientExists = m_clientInfoLookup.TryGetValue(clientID, out TlsClientInfo clientInfo); clientPrincipal = clientExists ? clientInfo.ClientPrincipal : null; return clientExists; } /// /// Validates the specified . /// /// Configuration string to be validated. /// Port property is missing. /// Port property value is not between and . protected override void ValidateConfigurationString(string configurationString) { m_configData = configurationString.ParseKeyValuePairs(); // Derive desired IP stack based on specified "interface" setting, adding setting if it's not defined m_ipStack = Transport.GetInterfaceIPStack(m_configData); if (!m_configData.ContainsKey("port")) throw new ArgumentException($"Port property is missing (Example: {DefaultConfigurationString})"); if (!Transport.IsPortNumberValid(m_configData["port"])) throw new ArgumentOutOfRangeException(nameof(configurationString), $"Port number must be between {Transport.PortRangeLow} and {Transport.PortRangeHigh}"); } /// /// Sends data to the specified client asynchronously. /// /// ID of the client to which the data is to be sent. /// The buffer that contains the binary data to be sent. /// The zero-based position in the at which to begin sending data. /// The number of bytes to be sent from starting at the . /// for the asynchronous operation. protected override WaitHandle SendDataToAsync(Guid clientID, byte[] data, int offset, int length) { if (!m_clientInfoLookup.TryGetValue(clientID, out TlsClientInfo clientInfo)) throw new InvalidOperationException($"No client found for ID {clientID}."); ConcurrentQueue sendQueue = clientInfo.SendQueue; // Execute operation to see if the client has reached the maximum send queue size. clientInfo.DumpPayloadsOperation.TryRun(); // Prepare for payload-aware transmission. if (PayloadAware) Payload.AddHeader(ref data, ref offset, ref length, m_payloadMarker, m_payloadEndianOrder); // Create payload and wait handle. TlsServerPayload payload = FastObjectFactory.CreateObjectFunction(); ManualResetEventSlim handle = FastObjectFactory.CreateObjectFunction(); payload.Data = data; payload.Offset = offset; payload.Length = length; payload.WaitHandle = handle; payload.ClientInfo = clientInfo; handle.Reset(); // Queue payload for sending. sendQueue.Enqueue(payload); lock (clientInfo.SendLock) { // Send next queued payload. if (Interlocked.CompareExchange(ref clientInfo.Sending, 1, 0) == 0) { if (sendQueue.TryDequeue(out TlsServerPayload dequeuedPayload)) ThreadPool.QueueUserWorkItem(state => SendPayload((TlsServerPayload)state), dequeuedPayload); else Interlocked.Exchange(ref clientInfo.Sending, 0); } } // Notify that the send operation has started. OnSendClientDataStart(clientID); // Return the async handle that can be used to wait for the async operation to complete. return handle.WaitHandle; } /// /// Callback method for asynchronous accept operation. /// private void ProcessAccept(SocketAsyncEventArgs acceptArgs) { TransportProvider client = new TransportProvider(); TlsClientInfo clientInfo = null; try { if (CurrentState == ServerState.NotRunning) return; // If acceptArgs was disposed, m_acceptArgs will either // be null or another instance of SocketAsyncEventArgs. // This check will tell us whether it's been disposed. if (acceptArgs != m_acceptArgs) return; SocketError error = acceptArgs.SocketError; if (error != SocketError.Success && error != SocketError.ConnectionReset) { // Error is unrecoverable. // We need to make sure to restart the // server before we throw the error. ThreadPool.QueueUserWorkItem(state => ReStart()); throw new SocketException((int)error); } // At this point, we have determined that the server is up and running. // We need to make sure the acceptArgs.AcceptAsync() method is called or // else the server will continue running but stop accepting connections try { if (acceptArgs.SocketError != SocketError.Success) throw new SocketException((int)error); if (MaxClientConnections != -1 && ClientIDs.Length >= MaxClientConnections) { // Reject client connection since limit has been reached. TerminateConnection(client, false); } else { // Process the newly connected client. LoadTrustedCertificates(); NetworkStream netStream = new NetworkStream(acceptArgs.AcceptSocket, true); client.Provider = new TlsSocket { Socket = acceptArgs.AcceptSocket, SslStream = new SslStream(netStream, false, RemoteCertificateValidationCallback ?? CertificateChecker.ValidateRemoteCertificate, LocalCertificateSelectionCallback), RemoteEndPoint = acceptArgs.AcceptSocket.RemoteEndPoint as IPEndPoint }; client.Provider.Socket.ReceiveBufferSize = ReceiveBufferSize; clientInfo = new TlsClientInfo { Client = client, SendLock = new object(), SendQueue = new ConcurrentQueue() }; // Create operation to dump send queue payloads when the queue grows too large. clientInfo.DumpPayloadsOperation = new ShortSynchronizedOperation(() => { // Check to see if the client has reached the maximum send queue size. if (MaxSendQueueSize > 0 && clientInfo.SendQueue.Count >= MaxSendQueueSize) { for (int i = 0; i < MaxSendQueueSize; i++) { if (clientInfo.SendQueue.TryDequeue(out TlsServerPayload payload)) { payload.WaitHandle.Set(); payload.WaitHandle.Dispose(); payload.WaitHandle = null; } } throw new InvalidOperationException($"Client {clientInfo.Client.ID} connected to TCP server reached maximum send queue size. {MaxSendQueueSize} payloads dumped from the queue."); } }, ex => OnSendClientDataException(clientInfo.Client.ID, ex)); clientInfo.TimeoutToken = new Action(() => client.Provider?.Socket.Dispose()).DelayAndExecute(15000); client.Provider.SslStream.BeginAuthenticateAsServer(Certificate, RequireClientCertificate, m_enabledSslProtocols, CheckCertificateRevocation, ProcessTlsAuthentication, clientInfo); } } finally { // Return to accepting new connections. acceptArgs.AcceptSocket = null; if (!Server.AcceptAsync(acceptArgs)) { ThreadPool.QueueUserWorkItem(state => ProcessAccept(acceptArgs)); } } } catch (ObjectDisposedException) { // m_acceptArgs may be disposed while in the middle of accepting a connection } catch (Exception ex) { // Exception occurred so make sure we cancel the timeout clientInfo?.TimeoutToken.Cancel(); // Notify of the exception. IPEndPoint remoteEndPoint = client.Provider?.RemoteEndPoint; string clientAddress = remoteEndPoint?.Address.ToString() ?? "UNKNOWN"; string errorMessage = $"Unable to accept connection to client [{clientAddress}]: {ex.Message}"; OnClientConnectingException(new Exception(errorMessage, ex)); TerminateConnection(client, false); } } /// /// Callback method for asynchronous authenticate operation. /// private void ProcessTlsAuthentication(IAsyncResult asyncResult) { TlsClientInfo clientInfo = (TlsClientInfo)asyncResult.AsyncState; TransportProvider client = clientInfo.Client; SslStream stream = client.Provider.SslStream; try { if (!clientInfo.TimeoutToken.Cancel()) throw new SocketException((int)SocketError.TimedOut); stream.EndAuthenticateAsServer(asyncResult); if (EnabledSslProtocols != SslProtocols.None) { if (!stream.IsAuthenticated) throw new InvalidOperationException("Unable to authenticate."); if (!stream.IsEncrypted) throw new InvalidOperationException("Unable to encrypt data stream."); } if (IntegratedSecurity) { #if !MONO clientInfo.NegotiateStream = new NegotiateStream(stream, true); clientInfo.TimeoutToken = new Action(() => client.Provider?.Socket.Dispose()).DelayAndExecute(15000); clientInfo.NegotiateStream.BeginAuthenticateAsServer(ProcessIntegratedSecurityAuthentication, clientInfo); #endif } else { // We can proceed further with receiving data from the client. m_clientInfoLookup.TryAdd(client.ID, clientInfo); OnClientConnected(client.ID); ReceivePayloadAsync(client); } } catch (Exception ex) { // Exception occurred so make sure we cancel the timeout clientInfo.TimeoutToken.Cancel(); // Notify of the exception. IPEndPoint remoteEndPoint = client.Provider.RemoteEndPoint; string clientAddress = remoteEndPoint.Address.ToString(); string errorMessage = $"Unable to authenticate connection to client [{clientAddress}]: {CertificateChecker.ReasonForFailure ?? ex.Message}"; OnClientConnectingException(new Exception(errorMessage, ex)); TerminateConnection(client, false); } } #if !MONO private void ProcessIntegratedSecurityAuthentication(IAsyncResult asyncResult) { TlsClientInfo clientInfo = (TlsClientInfo)asyncResult.AsyncState; TransportProvider client = clientInfo.Client; NegotiateStream negotiateStream = clientInfo.NegotiateStream; IPEndPoint remoteEndPoint = client.Provider.RemoteEndPoint; try { if (!clientInfo.TimeoutToken.Cancel()) throw new SocketException((int)SocketError.TimedOut); try { negotiateStream.EndAuthenticateAsServer(asyncResult); if (negotiateStream.RemoteIdentity is WindowsIdentity identity) { WindowsPrincipal clientPrincipal = new WindowsPrincipal(identity); clientInfo.ClientPrincipal = clientPrincipal; } } catch (InvalidCredentialException) { if (!IgnoreInvalidCredentials) throw; } // We can proceed further with receiving data from the client. m_clientInfoLookup.TryAdd(client.ID, clientInfo); OnClientConnected(client.ID); ReceivePayloadAsync(client); } catch (Exception ex) { // Notify of the exception. string clientAddress = remoteEndPoint.Address.ToString(); string errorMessage = $"Unable to authenticate connection to client [{clientAddress}]: {ex.Message}"; OnClientConnectingException(new Exception(errorMessage, ex)); TerminateConnection(client, false); } finally { negotiateStream.Dispose(); } } #endif /// /// Asynchronous loop sends payloads on the socket. /// private void SendPayload(TlsServerPayload payload) { TlsClientInfo clientInfo = null; TransportProvider client = null; //ManualResetEventSlim handle; try { clientInfo = payload.ClientInfo; client = clientInfo.Client; //handle = payload.WaitHandle; byte[] data = payload.Data; int offset = payload.Offset; int length = payload.Length; // Send payload to the client asynchronously. client.Provider.SslStream.BeginWrite(data, offset, length, ProcessSend, payload); } catch (Exception ex) { if (client != null) OnSendClientDataException(client.ID, ex); if (clientInfo != null) { // Assume process send was not able // to continue the asynchronous loop. Interlocked.Exchange(ref clientInfo.Sending, 0); } } } /// /// Callback method for asynchronous send operation. /// private void ProcessSend(IAsyncResult asyncResult) { TlsServerPayload payload = null; TlsClientInfo clientInfo = null; TransportProvider client = null; ConcurrentQueue sendQueue = null; try { payload = (TlsServerPayload)asyncResult.AsyncState; clientInfo = payload.ClientInfo; client = clientInfo.Client; sendQueue = clientInfo.SendQueue; ManualResetEventSlim handle = payload.WaitHandle; // Set the wait handle to indicate // the send operation is complete. handle.Set(); // Update statistics and notify. client.Provider.SslStream.EndWrite(asyncResult); client.Statistics.UpdateBytesSent(payload.Length); OnSendClientDataComplete(client.ID); } catch (Exception ex) { // Send operation failed to complete. if (client != null) OnSendClientDataException(client.ID, ex); } finally { if (payload != null && sendQueue != null) { try { payload.WaitHandle = null; payload.ClientInfo = null; // Begin sending next client payload. if (sendQueue.TryDequeue(out payload)) { ThreadPool.QueueUserWorkItem(state => SendPayload((TlsServerPayload)state), payload); } else if (clientInfo != null) { lock (clientInfo.SendLock) { if (sendQueue.TryDequeue(out payload)) ThreadPool.QueueUserWorkItem(state => SendPayload((TlsServerPayload)state), payload); else Interlocked.Exchange(ref clientInfo.Sending, 0); } } } catch (Exception ex) { string errorMessage = $"Exception encountered while attempting to send next payload: {ex.Message}"; if (client != null) OnSendClientDataException(client.ID, new Exception(errorMessage, ex)); if (clientInfo != null) Interlocked.Exchange(ref clientInfo.Sending, 0); } } } } /// /// Initiate method for asynchronous receive operation of payload data. /// private void ReceivePayloadAsync(TransportProvider client) { // Initialize bytes received. client.BytesReceived = 0; // Initiate receiving. if (PayloadAware) { // Payload boundaries are to be preserved. client.SetReceiveBuffer(m_payloadMarker.Length + Payload.LengthSegment); ReceivePayloadAwareAsync(client, true); } else { // Payload boundaries are not to be preserved. client.SetReceiveBuffer(ReceiveBufferSize); ReceivePayloadUnawareAsync(client); } } /// /// Initiate method for asynchronous receive operation of payload data in "payload-aware" mode. /// private void ReceivePayloadAwareAsync(TransportProvider client, bool waitingForHeader) { client?.Provider?.SslStream?.BeginRead(client.ReceiveBuffer, client.BytesReceived, client.ReceiveBufferSize - client.BytesReceived, ProcessReceivePayloadAware, new Tuple(client.ID, waitingForHeader)); } /// /// Callback method for asynchronous receive operation of payload data in "payload-aware" mode. /// private void ProcessReceivePayloadAware(IAsyncResult asyncResult) { Tuple asyncState = (Tuple)asyncResult.AsyncState; bool waitingForHeader = asyncState.Item2; if (!TryGetClient(asyncState.Item1, out TransportProvider client) || client is null) return; try { if (client.Provider?.SslStream is null) return; // Update statistics and pointers. client.Statistics.UpdateBytesReceived(client.Provider.SslStream.EndRead(asyncResult)); client.BytesReceived += client.Statistics.LastBytesReceived; if (!client.Provider.Socket.Connected) throw new SocketException((int)SocketError.Disconnecting); if (client.Statistics.LastBytesReceived == 0) throw new SocketException((int)SocketError.Disconnecting); if (waitingForHeader) { // We're waiting on the payload length, so we'll check if the received data has this information. int payloadLength = Payload.ExtractLength(client.ReceiveBuffer, client.BytesReceived, m_payloadMarker, m_payloadEndianOrder); // We have the payload length. // If it is set to zero, there is no payload; wait for another header. // Otherwise we'll create a buffer that's big enough to hold the entire payload. if (payloadLength == 0) { client.BytesReceived = 0; } else if (payloadLength != -1) { client.BytesReceived = 0; client.SetReceiveBuffer(payloadLength); waitingForHeader = false; } ReceivePayloadAwareAsync(client, waitingForHeader); } else { // We're accumulating the payload in the receive buffer until the entire payload is received. if (client.BytesReceived == client.ReceiveBufferSize) { // We've received the entire payload. OnReceiveClientDataComplete(client.ID, client.ReceiveBuffer, client.BytesReceived); ReceivePayloadAsync(client); } else { // We've not yet received the entire payload. ReceivePayloadAwareAsync(client, false); } } } catch (ObjectDisposedException) { // Make sure connection is terminated when server is disposed. TerminateConnection(client, true); } catch (SocketException ex) { // Terminate connection when socket exception is encountered. OnReceiveClientDataException(client.ID, ex); TerminateConnection(client, true); } catch (Exception ex) { try { // For any other exception, notify and resume receive. OnReceiveClientDataException(client.ID, ex); ReceivePayloadAsync(client); } catch { // Terminate connection if resuming receiving fails. TerminateConnection(client, true); } } } /// /// Initiate method for asynchronous receive operation of payload data in "payload-unaware" mode. /// private void ReceivePayloadUnawareAsync(TransportProvider client) { client?.Provider?.SslStream?.BeginRead(client.ReceiveBuffer, 0, client.ReceiveBufferSize, ProcessReceivePayloadUnaware, client); } /// /// Callback method for asynchronous receive operation of payload data in "payload-unaware" mode. /// private void ProcessReceivePayloadUnaware(IAsyncResult asyncResult) { TransportProvider client = (TransportProvider)asyncResult.AsyncState; try { // Update statistics and pointers. client.Statistics.UpdateBytesReceived(client.Provider.SslStream.EndRead(asyncResult)); client.BytesReceived = client.Statistics.LastBytesReceived; if (!client.Provider.Socket.Connected) throw new SocketException((int)SocketError.Disconnecting); if (client.Statistics.LastBytesReceived == 0) throw new SocketException((int)SocketError.Disconnecting); // Notify of received data and resume receive operation. OnReceiveClientDataComplete(client.ID, client.ReceiveBuffer, client.BytesReceived); ReceivePayloadUnawareAsync(client); } catch (ObjectDisposedException) { // Make sure connection is terminated when server is disposed. TerminateConnection(client, true); } catch (SocketException ex) { // Terminate connection when socket exception is encountered. OnReceiveClientDataException(client.ID, ex); TerminateConnection(client, true); } catch (Exception ex) { try { // For any other exception, notify and resume receive. OnReceiveClientDataException(client.ID, ex); ReceivePayloadAsync(client); } catch { // Terminate connection if resuming receiving fails. TerminateConnection(client, true); } } } /// /// Processes the termination of client. /// private void TerminateConnection(TransportProvider client, bool raiseEvent) { client.Reset(); if (raiseEvent) OnClientDisconnected(client.ID); m_clientInfoLookup.TryRemove(client.ID, out TlsClientInfo _); } /// /// Returns the certificate set by the user. /// private X509Certificate DefaultLocalCertificateSelectionCallback(object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers) { return Certificate; } /// /// Loads the list of trusted certificates into the default certificate checker. /// private void LoadTrustedCertificates() { if (RemoteCertificateValidationCallback != null || m_certificateChecker != null) return; m_defaultCertificateChecker.TrustedCertificates.Clear(); string trustedCertificatesPath = FilePath.AddPathSuffix(FilePath.GetAbsolutePath(TrustedCertificatesPath)); if (!Directory.Exists(trustedCertificatesPath)) return; foreach (string fileName in FilePath.GetFileList(trustedCertificatesPath)) m_defaultCertificateChecker.TrustedCertificates.Add(new X509Certificate2(fileName)); } #endregion } }