Как разрешить серверу принимать как SSL, так и обычные текстовые (небезопасные) соединения?

Я пытаюсь создать сервер, который может принимать как безопасное SSL, так и небезопасное текстовое соединение (для обратной совместимости). Мой код почти работает, за исключением того, что первые переданные данные, полученные от небезопасного клиента, теряют первые 5 байтов (символов) на сервере. В частности, если я передаю 30 байтов по незащищенному соединению, когда сервер получает функцию OnClientDataReceived(), строка «int iRx = nwStream.EndRead(asyn);», затем iRx = 25. Любые последующие сообщения, передаваемые от клиента, содержат все отправленные байты/символы, как и ожидалось. Я подозреваю, что первоначальное предположение о том, что соединение является SSLStream, может заключаться в удалении первых 5 байтов, а затем, когда оно терпит неудачу, эти 5 байтов уже были извлечены из буфера и больше не доступны. Кто-нибудь знает о другом подходе, который я мог бы использовать для написания кода, чтобы сервер мог автоматически переключаться на лету?

Я стараюсь избегать следующих действий:

  • Требовать, чтобы клиент подключался с использованием простого текста NetworkStream, а затем запрашивал обновление до потока SSL.
  • Настройка двух TcpListeners на двух разных портах (один для безопасного, один для небезопасного)

Вот мой код:

/// Each client that connects gets an instance of the ConnectedClient class.
Class Pseudo_ConnectedClient
{
    //Properties
    byte[] Buffer; //Holds temporary buffer of read bytes from BeginRead()
    TcpClient TCPClient; //Reference to the connected client
    Socket ClientSocket; //The outer Socket Reference of the connected client
    StringBuilder CurrentMessage; //concatenated chunks of data in buffer until we have a complete message (ends with <ETX>
    Stream Stream; //SSLStream or NetworkStream depending on client
    ArrayList MessageQueue; //Array of complete messages received from client that need to be processed
}

/// When a new client connects (OnClientConnection callback is executed), the server creates the ConnectedClient object and stores its 
/// reference in a local dictionary, then configures the callbacks for incoming data (WaitForClientData)
void OnClientConnection(IAsyncResult result)
{
    TcpListener listener = result.AsyncState as TcpListener;
    TcpClient clnt = null;

    try
    {
        if (!IsRunning) //then stop was called, so don't call EndAcceptTcpClient because it will throw and ObjectDisposedException
            return;

        //Start accepting the next connection...
        listener.BeginAcceptTcpClient(this.onClientConnection, listener);

        //Get reference to client and set flag to indicate connection accepted.
        clnt = listener.EndAcceptTcpClient(result);

        //Add the reference to our ArrayList of Connected Clients
        ConnectedClient conClnt = new ConnectedClient(clnt);
        _clientList.Add(conClnt);

        //Configure client to listen for incoming data
        WaitForClientData(conClnt);
    }
    catch (Exception ex)
    {
        Trace.WriteLine("Server:OnClientConnection: Exception - " + ex.ToString());
    }
}

/// WaitForClientData registers the AsyncCallback to handle incoming data from a client (OnClientDataReceieved).  
/// If a certificate has been provided, then it listens for clients to connect on an SSLStream and configures the 
/// BeginAuthenticateAsServer callback.  If no certificate is provided, then it only sets up a NetworkStream 
/// and prepares for the BeginRead callback.
private void WaitForClientData(ConnectedClient clnt)
{
    if (!IsRunning) return; //Then stop was called, so don't do anything

    SslStream sslStream = null;

    try
    {
        if (_pfnClientDataCallBack == null) //then define the call back function to invoke when data is received from a connected client
            _pfnClientDataCallBack = new AsyncCallback(OnClientDataReceived);

        NetworkStream nwStream = clnt.TCPClient.GetStream();

        //Check if we can establish a secure connection
        if (this.SSLCertificate != null) //Then we have the ability to make an SSL connection (SSLCertificate is a X509Certificate2 object)
        {
            if (this.certValidationCallback != null)
                sslStream = new SslStream(nwStream, true, this.certValidationCallback);
            else
                sslStream = new SslStream(nwStream, true);

            clnt.Stream = sslStream;

            //Start Listening for incoming (secure) data
            sslStream.BeginAuthenticateAsServer(this.SSLCertificate, false, SslProtocols.Default, false, onAuthenticateAsServer, clnt);
        }
        else //No certificate available to make a secure connection, so use insecure (unless not allowed)
        {
            if (this.RequireSecureConnection == false) //Then we can try to read from the insecure stream
            {
                clnt.Stream = nwStream;

                //Start Listening for incoming (unsecure) data
                nwStream.BeginRead(clnt.Buffer, 0, clnt.Buffer.Length, _pfnClientDataCallBack, clnt);
            }
            else //we can't do anything - report config problem
            {
                throw new InvalidOperationException("A PFX certificate is not loaded and the server is configured to require a secure connection");
            }
        }
    }
    catch (Exception ex)
    {
        DisconnectClient(clnt);
    }
}

/// OnAuthenticateAsServer first checks if the stream is authenticated, if it isn't it gets the TCPClient's reference 
/// to the outer NetworkStream (client.TCPClient.GetStream()) - the insecure stream and calls the BeginRead on that.  
/// If the stream is authenticated, then it keeps the reference to the SSLStream and calls BeginRead on it.
private void OnAuthenticateAsServer(IAsyncResult result)
{
    ConnectedClient clnt = null;
    SslStream sslStream = null;

    if (this.IsRunning == false) return;

    try
    {
        clnt = result.AsyncState as ConnectedClient;
        sslStream = clnt.Stream as SslStream;

        if (sslStream.IsAuthenticated)
            sslStream.EndAuthenticateAsServer(result);
        else //Try and switch to an insecure connections
        {
            if (this.RequireSecureConnection == false) //Then we are allowed to accept insecure connections
            {
                if (clnt.TCPClient.Connected)
                    clnt.Stream = clnt.TCPClient.GetStream();
            }
            else //Insecure connections are not allowed, close the connection
            {
                DisconnectClient(clnt);
            }
        }
    }
    catch (Exception ex)
    {
        DisconnectClient(clnt);
    }

    if( clnt.Stream != null) //Then we have a stream to read, start Async read
        clnt.Stream.BeginRead(clnt.Buffer, 0, clnt.Buffer.Length, _pfnClientDataCallBack, clnt);
}

/// OnClientDataReceived callback is triggered by the BeginRead async when data is available from a client.  
/// It determines if the stream (as assigned by OnAuthenticateAsServer) is an SSLStream or a NetworkStream 
/// and then reads the data out of the stream accordingly.  The logic to parse and process the message has 
/// been removed because it isn't relevant to the question.
private void OnClientDataReceived(IAsyncResult asyn)
{
    try
    {
        ConnectedClient connectClnt = asyn.AsyncState as ConnectedClient;

        if (!connectClnt.TCPClient.Connected) //Then the client is no longer connected >> clean up
        {
            DisconnectClient(connectClnt);
            return;
        }

        Stream nwStream = null;
        if( connectClnt.Stream is SslStream) //Then this client is connected via a secure stream
            nwStream = connectClnt.Stream as SslStream;
        else //this is a plain text stream
            nwStream = connectClnt.Stream as NetworkStream;

        // Complete the BeginReceive() asynchronous call by EndReceive() method which
        // will return the number of characters written to the stream by the client
        int iRx = nwStream.EndRead(asyn); //Returns the numbers of bytes in the read buffer
        char[] chars = new char[iRx];   

        // Extract the characters as a buffer and create a String
        Decoder d = ASCIIEncoding.UTF8.GetDecoder();
        d.GetChars(connectClnt.Buffer, 0, iRx, chars, 0);

        //string data = ASCIIEncoding.ASCII.GetString(buff, 0, buff.Length);
        string data = new string(chars);

        if (iRx > 0) //Then there was data in the buffer
        {
            //Append the current packet with any additional data that was already received
            connectClnt.CurrentMessage.Append(data);

            //Do work here to check for a complete message
            //Make sure two complete messages didn't get concatenated in one transmission (mobile devices)
            //Add each message to the client's messageQueue
            //Clear the currentMessage
            //Any partial messsage at the end of the buffer needs to be added to the currentMessage

            //Start reading again
            nwStream.BeginRead(connectClnt.Buffer, 0, connectClnt.Buffer.Length, OnClientDataReceived, connectClnt);
        }
        else //zero-length packet received - Disconnecting socket
        {
            DisconnectClient(connectClnt);
        }                
    }
    catch (Exception ex)
    {
        return;
    }
}

Что работает:

  • Если у сервера нет сертификата, используется только NetworkStream, и все байты принимаются от клиента для всех сообщений.
  • Если у сервера есть сертификат (настроен SSLStream) и может быть установлено безопасное соединение (веб-браузер с использованием https://), и для всех сообщений будет получено полное сообщение.

Что не работает:

  • Если у сервера есть сертификат (настроен SSLStream) и от клиента установлено небезопасное соединение, при получении первого сообщения от этого клиента код правильно определяет, что SSLStream не прошел проверку подлинности, и переключается на NetworkStream из TCPClient. Однако, когда EndRead вызывается для этого NetworkStream для первого сообщения, первые 5 символов (байтов) отсутствуют в отправленном сообщении, но только для первого сообщения. Все остальные сообщения завершены, пока TCPClient подключен. Если клиент отключается, а затем снова подключается, первое сообщение обрезается, после чего все последующие сообщения снова являются правильными.

Что вызывает обрезание этих первых 5 байтов и как этого избежать?

Мой проект в настоящее время использует .NET v3.5... Я хотел бы остаться на этой версии и не переходить на 4.0, если я могу этого избежать.


Дополнительный вопрос

Ответ Дэмиена ниже позволяет мне сохранить эти недостающие 5 байтов, однако я бы предпочел придерживаться методов BeginRead и EndRead в своем коде, чтобы избежать блокировки. Есть ли хорошие учебные пособия, показывающие «лучшие методы» при их переопределении? Точнее, как работать с объектом IAsyncResult. Я понимаю, что мне нужно будет добавить любой контент, который хранится в буферах RestartableStream, а затем перейти во внутренний поток (базу), чтобы получить остальное и вернуть торал. Но поскольку объект IAsyncResult является пользовательским классом, я не могу понять общий способ, которым я могу комбинировать баффы RestartableStream с баффами внутреннего потока перед возвратом. Нужно ли мне также реализовывать BeginRead(), чтобы я знал, в каких буферах пользователь хочет хранить содержимое? Я предполагаю, что другое решение, поскольку проблема с отброшенными байтами связана только с первым сообщением от клиента (после этого я знаю, использовать ли его как SSLStream или NetworkStream), будет заключаться в том, чтобы обработать это первое сообщение, напрямую вызвав Read() метод RestartableStream (временно блокируя код), затем для всех будущих сообщений используйте асинхронные обратные вызовы для чтения содержимого, как я делаю сейчас.


person Jerren Saunders    schedule 05.03.2013    source источник
comment
почему вы не хотите открыть другой порт? вот что делает http(s).   -  person Daniel A. White    schedule 05.03.2013
comment
Открытие порта в брандмауэре нашей компании — долгий процесс. Сейчас у нас открыт один порт, и мы хотели бы использовать один и тот же порт как для безопасных, так и для небезопасных соединений, если это возможно.   -  person Jerren Saunders    schedule 05.03.2013
comment
То, что вы пытаетесь сделать, иногда называют объединением портов. Это довольно необычно и часто сбивает с толку. Другим способом было бы изменить ваш протокол, чтобы иметь команду STARTTLS; ряд протоколов делают это.   -  person Bruno    schedule 07.03.2013


Ответы (2)


Хорошо, я думаю, лучшее, что вы можете сделать, это поместить свой собственный класс между SslStream и NetworkStream, где вы реализуете некоторую настраиваемую буферизацию. Я выполнил несколько тестов для нижеприведенных, но я бы порекомендовал провести еще несколько, прежде чем запускать их в производство (и, возможно, более надежную обработку ошибок). Я думаю, что избежал 4.0 или 4.5измов:

  public sealed class RestartableReadStream : Stream
  {
    private Stream _inner;
    private List<byte[]> _buffers;
    private bool _buffering;
    private int? _currentBuffer = null;
    private int? _currentBufferPosition = null;
    public RestartableReadStream(Stream inner)
    {
      if (!inner.CanRead) throw new NotSupportedException(); //Don't know what else is being expected of us
      if (inner.CanSeek) throw new NotSupportedException(); //Just use the underlying streams ability to seek, no need for this class
      _inner = inner;
      _buffering = true;
      _buffers = new List<byte[]>();
    }

    public void StopBuffering()
    {
      _buffering = false;
      if (!_currentBuffer.HasValue)
      {
        //We aren't currently using the buffers
        _buffers = null;
        _currentBufferPosition = null;
      }
    }

    public void Restart()
    {
      if (!_buffering) throw new NotSupportedException();  //Buffering got turned off already
      if (_buffers.Count == 0) return;
      _currentBuffer = 0;
      _currentBufferPosition = 0;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
      if (_currentBuffer.HasValue)
      {
        //Try to satisfy the read request from the current buffer
        byte[] rbuffer = _buffers[_currentBuffer.Value];
        int roffset = _currentBufferPosition.Value;
        if ((rbuffer.Length - roffset) <= count)
        {
          //Just give them what we have in the current buffer (exhausting it)
          count = (rbuffer.Length - roffset);
          for (int i = 0; i < count; i++)
          {
            buffer[offset + i] = rbuffer[roffset + i];
          }

          _currentBuffer++;
          if (_currentBuffer.Value == _buffers.Count)
          {
            //We've stopped reading from the buffers
            if (!_buffering)
              _buffers = null;
            _currentBuffer = null;
            _currentBufferPosition = null;
          }
          return count;
        }
        else
        {
          for (int i = 0; i < count; i++)
          {
            buffer[offset + i] = rbuffer[roffset + i];
          }
          _currentBufferPosition += count;
          return count;
        }
      }
      //If we reach here, we're currently using the inner stream. But may be buffering the results
      int ncount = _inner.Read(buffer, offset, count);
      if (_buffering)
      {
        byte[] rbuffer = new byte[ncount];
        for (int i = 0; i < ncount; i++)
        {
          rbuffer[i] = buffer[offset + i];
        }
        _buffers.Add(rbuffer);
      }
      return ncount;
    }

    public override bool CanRead
    {
      get { return true; }
    }

    public override bool CanSeek
    {
      get { return false; }
    }

    public override bool CanWrite
    {
      get { return false; }
    }

    //No more interesting code below here

    public override void Flush()
    {
      throw new NotSupportedException();
    }

    public override long Length
    {
      get { throw new NotSupportedException(); }
    }

    public override long Position
    {
      get
      {
        throw new NotSupportedException();
      }
      set
      {
        throw new NotSupportedException();
      }
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
      throw new NotSupportedException();
    }

    public override void SetLength(long value)
    {
      throw new NotSupportedException();
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
      throw new NotSupportedException();
    }
  }

Использование:

Постройте RestartableReadStream вокруг своего NetworkStream. Передайте этот экземпляр SslStream. Если вы решите, что SSL — неправильный способ работы, позвоните по номеру Restart(), а затем снова используйте его, как хотите. Вы даже можете попробовать более двух стратегий (коллируя Restart() между каждой).

Как только вы определитесь, какая стратегия (например, SSL или не-SSL) является правильной, позвоните по номеру StopBuffering(). Как только он завершит воспроизведение всех доступных ему буферов, он вернется к простому вызову Read в своем внутреннем потоке. Если вы не вызываете StopBuffering, то вся история операций чтения из потока будет храниться в списке _buffers, что может значительно увеличить нагрузку на память.

Обратите внимание, что ни одно из вышеперечисленных действий не относится к многопоточному доступу. Но если у вас есть несколько потоков, вызывающих Read() в одном потоке (особенно сетевом), я все равно не ожидаю никакого здравомыслия.

person Damien_The_Unbeliever    schedule 05.03.2013
comment
Я реализовал ваш класс RestartableStream, и он определенно сохраняет потерянные 5 байтов! Спасибо!!! Однако у меня есть два дополнительных вопроса ... поскольку я новичок в переполнении, мне задать их в комментарии к вашему вопросу или начать новый вопрос и каким-то образом связать их с этим? - person Jerren Saunders; 06.03.2013
comment
@JerrenSaunders - если это всего лишь незначительное дополнение к тому, что уже было задано, и все еще напрямую связанное с исходным вопросом, в идеале вам следует отредактировать свой вопрос (ссылка для редактирования внизу) и добавить туда свои дополнительные вопросы. Я обычно говорю, что если это означает больше, чем добавление абзаца или двух, или менее связано с исходным вопросом, может быть лучше опубликовать как новый вопрос. К сожалению, нет универсального совета, который можно было бы дать по этому поводу, и, насколько я понимаю, все сводится к тому, насколько это связано и насколько близко к оригиналу. - person Damien_The_Unbeliever; 06.03.2013
comment
@JerrenSaunders - также, если вы считаете, что ответ помог, проголосуйте за него. Если он полностью отвечает на ваш вопрос (похоже, пока не отвечает), поставьте галочку рядом с ним. - person Damien_The_Unbeliever; 06.03.2013
comment
Это почти так. Один вопрос более уместно задать в качестве комментария (другой я добавлю в качестве дополнения): SslStream() требует, чтобы данный поток был доступен для чтения/записи. Я заметил, что вы установили для свойства CanWrite значение false для своего класса. Если я проверю, что соединение является SSLStream, и использую SSLStream.Write() для этих клиентов и использую RestartableStream.Write()=>base.Write() для клиентов, которые являются обычным NetworkStream (обычный текст ), есть ли какие-либо проблемы, которые я мог упустить из виду, а вы пытались избежать? (У меня сегодня сильный насморк, поэтому я немного затуманен). - person Jerren Saunders; 06.03.2013
comment
@JerrenSaunders - вы можете изменить приведенный выше класс, чтобы разрешить вызовы Write(), но я бы рекомендовал: а) вызвать исключение, если вы сейчас воспроизводите (например, _currentBuffer.HasValue верно), и б) установить _buffering false. (А затем измените мой код, чтобы он вызывал _inner.CanWrite внутри моего CanWrite). Я не могу указать на какие-либо конкретные проблемы, которые возникнут без этого, просто немного жутковато, если что-то Write работает во время воспроизведения частей потока, которые уже были прочитаны хотя бы один раз ранее. - person Damien_The_Unbeliever; 06.03.2013
comment
В ПОРЯДКЕ. В этом есть смысл. В моем случае я намерен писать (отвечать) клиенту (через поток) только после того, как сервер определил правильный тип потока. Таким образом, к этому моменту был вызван метод StopBuffering и буферы очищены. Но хороший момент для защиты от таких ситуаций на случай, если этот класс будет повторно использоваться в будущем. - person Jerren Saunders; 06.03.2013
comment
Плохие новости. Я был так сосредоточен на восстановлении недостающих 5 байтов для небезопасных соединений, что не стал повторно тестировать безопасное соединение. Непосредственно перед выполнением обратного вызова SSLStream OnAuthenticateAsServer вызывается метод RestartableStream Write... Я предполагаю, что он выполняет квитирование SSL. В базовом потоке нет метода Write, к которому я могу просто перейти, только метод WriteByte... однако, если я попытаюсь выполнить итерацию по заданному буферу и вызову: foreach(...){ base.WriteByte(buffer[i]);}, он фактически сделает рекурсивный вызов RestartableStream.Write (переопределенный способ)... :-( - person Jerren Saunders; 13.03.2013
comment
Неважно... вместо вызова base.WriteByte() я понял, что мне нужно вызвать метод Write() внутреннего потока. Плохо быть таким медленным в среду... - person Jerren Saunders; 13.03.2013

Я потратил часы на то, чтобы не писать оболочку потока для NetworkStream, и, наконец, наткнулся на это, и это сработало для меня. MSDN SocketFlag.Peek Я продолжал находить предложения просто написать оболочку или использовать отдельные порты, но у меня возникли проблемы с прислушиванием к авторитету или разуму.

Вот мой код. NLOLOL (без смеха или лекций) Я не совсем понял, нужно ли мне заглядывать больше, чем в первый байт для всех сценариев.

Private Async Sub ProcessTcpClient(__TcpClient As Net.Sockets.TcpClient)

        If __TcpClient Is Nothing OrElse Not __TcpClient.Connected Then Return

        Dim __RequestBuffer(0) As Byte
        Dim __BytesRead As Integer

        Using __NetworkStream As Net.Sockets.NetworkStream = __TcpClient.GetStream

            __BytesRead = __TcpClient.Client.Receive(__RequestBuffer, 0, 1, SocketFlags.Peek)
            If __BytesRead = 1 AndAlso __RequestBuffer(0) = 22 Then
                Await Me.ProcessTcpClientSsl(__NetworkStream)
            Else
                Await Me.ProcessTcpClientNonSsl(__NetworkStream)
            End If

        End Using

        __TcpClient.Close()

End Sub
person Big Dummy    schedule 09.07.2015
comment
спасибо за упорство ;-). Это очень помогло мне понять, как я могу изменить свой поток с non ssl на ssl. Обертки мне тоже ничем не помогли. Еще раз спасибо! - person Stormer; 03.09.2019