如何允许服务器接受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  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未经过身份validation并切换到的NetworkStream TCPClient 。 但是,当在该NetworkStream为第一条消息调用EndRead时,发送的消息中缺少前5个字符(字节),但仅针对第一条消息。 只要连接了TCPClient所有剩余的消息就完成了。 如果客户端断开连接然后重新连接,则会剪切第一条消息,然后所有后续消息再次正常。

是什么导致前5个字节被剪裁,我该如何避免它?

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


后续问题

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

好的,我认为您可以做的最好的事情就是将自己的类放在SslStreamNetworkStream之间,在那里实现一些自定义缓冲。 我已经在下面做了一些测试,但是在你投入生产之前我会推荐一些(可能还有一些更强大的error handling)。 我我已经避免任何4.0或4.5主义:

  public sealed class RestartableReadStream : Stream { private Stream _inner; private List _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(); } 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列表中,这可能会增加相当多的内存压力。

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

我花了几个小时搜索不在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