【问题标题】:How to allow a Server to accept both SSL and plain text (insecure) connections?如何允许服务器同时接受 SSL 和纯文本(不安全)连接?
【发布时间】:2013-03-05 15:21:31
【问题描述】:

我正在尝试创建一个可以接受安全 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:// 的 Web 浏览器)并且收到所有消息的完整消息。

什么不起作用:

  • 如果服务器确实有证书(设置了SSLStream)并且从客户端建立了不安全的连接,那么当从该客户端接收到第一条消息时,代码会正确检测到SSLStream 未经过身份验证并切换到TCPClient 的NetworkStream。但是,当在 NetworkStream 上为第一条消息调用 EndRead 时,发送的消息中缺少前 5 个字符(字节),但仅针对第一条消息。只要连接了TCPClient,所有剩余的消息都是完整的。如果客户端断开连接然后重新连接,则第一条消息被剪裁,然后所有后续消息再次正常。

是什么导致前 5 个字节被截断,我该如何避免?

我的项目目前正在使用 .NET v3.5...如果可以避免的话,我想保持在这个版本,不要升级到 4.0。


跟进问题

Damien 在下面的回答确实允许我保留那些丢失的 5 个字节,但是,我更愿意在我的代码中坚持使用 BeginRead 和 EndRead 方法以避免阻塞。是否有任何好的教程在覆盖这些时显示“最佳实践”?更具体地说,如何使用 IAsyncResult 对象。我知道我需要添加存储在 RestartableStream 缓冲区中的任何内容,然后进入内部流(基础)以获取其余内容并返回 toral。但是由于IAsyncResult 对象是一个自定义类,我无法弄清楚在返回之前可以将 RestartableStream 的 buff 与内部流的 buff 结合起来的通用方法。我是否还需要实现 BeginRead() 以便我知道用户希望将内容存储到的缓冲区?我猜另一种解决方案是,因为丢失字节问题仅与来自客户端的第一条消息有关(之后我知道是否将其用作SSLStream 或NetworkStream),将通过以下方式处理第一条消息直接调用 RestartableStream 的Read() 方法(暂时阻塞代码),然后对于所有未来的消息使用异步回调读取内容,就像我现在一样。

【问题讨论】:

  • 你为什么不想打开另一个端口?这就是 http(s) 的作用。
  • 在我们公司的防火墙上打开一个端口是一个漫长的过程。我们现在打开了一个端口,如果可能的话,我们希望将同一个端口用于安全和不安全的连接。
  • 您尝试做的事情有时被称为“端口统一”。这很不常见,而且经常令人困惑。另一种方法是将您的协议更改为具有“STARTTLS”命令;许多协议都可以做到这一点。

标签: c# ssl client-server networkstream sslstream


【解决方案1】:

好的,我认为您能做的最好的事情就是将您自己的类放在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();
    }
  }

用法:

围绕您的NetworkStream 构造一个RestartableReadStream。将该实例传递给SslStream。如果您认为 SSL 是错误的做事方式,请致电 Restart(),然后根据需要再次使用它。您甚至可以尝试两种以上的策略(在每种策略之间调用Restart())。

一旦您确定哪种策略(例如 SSL 或非 SSL)是正确的,请致电 StopBuffering()。一旦它完成重放它可用的任何缓冲区,它将恢复为仅在其内部流上调用Read。如果您不调用StopBuffering,则从流中读取的整个历史记录将保存在_buffers 列表中,这可能会增加相当多的内存压力。

请注意,以上都没有特别考虑到多线程访问。但是,如果您有多个线程在单个流上调用 Read()(尤其是基于网络的流),那么无论如何我都不会期望任何理智。

【讨论】:

  • 我实现了你的 RestartableStream 类,它肯定保留了丢失的 5 个字节!谢谢!!!不过,我有两个后续问题......因为我是新来的溢出,我是在对你的问题的评论中问他们,还是开始一个新问题并以某种方式将它们链接到这个问题?
  • @JerrenSaunders - 如果它只是对已经提出的问题的一个小补充,并且仍然与原始问题直接相关,那么您最好编辑您的问题(底部有一个编辑链接)并添加您的那里的后续问题。我通常会说,如果这不仅仅是添加一两段,或者与原始问题的相关性较小,那么作为新问题发布可能会更好。不幸的是,就我而言,没有“一刀切”的建议可以给出,它归结为与原作的相关程度和接近程度。
  • @JerrenSaunders - 另外,如果您觉得答案有帮助,请点赞。如果它完全回答了您的问题(听起来还不像),请点击它旁边的复选标记。
  • 几乎可以。一个问题更适合作为评论提出(另一个我将作为后续问题提出): SslStream() 要求给定的流是读/写的。我注意到你强制你的类的 CanWrite 属性为 false 。如果我验证连接是 SSLStream,并为这些客户端使用 SSLStream.Write(),并为只是常规 NetworkStream(纯文本)的客户端使用 RestartableStream.Write()=>base.Write() ),是否有任何我可能忽略的问题,而您试图避免这些问题? (我今天得了重感冒,所以有点迷糊)。
  • @JerrenSaunders - 您可以修改上述类以允许发生 Write() 调用 - 但我建议 a)如果您当前正在重播​​,则引发异常(例如 _currentBuffer.HasValue 是true),并且 b) 你设置了_buffering false。 (然后修改我的代码以在我的CanWrite 中调用_inner.CanWrite)。我无法指出不这样做会发生的任何具体问题,如果在重播之前已经至少读过一次的部分流时出现Writeing,那只会让人感觉有点毛骨悚然。
【解决方案2】:

我花了几个小时寻找不围绕 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

【讨论】:

  • 感谢您的固执;-)。它极大地帮助了我弄清楚如何将我的流从非 ssl 更改为 ssl。包装器也没有为我提供任何帮助。再次感谢!
猜你喜欢
  • 2019-01-07
  • 2012-08-12
  • 1970-01-01
  • 2014-09-15
  • 2020-03-01
  • 2014-01-17
  • 1970-01-01
  • 1970-01-01
  • 2013-06-27
相关资源
最近更新 更多