安全なSSLと安全でないプレーンテキスト接続の両方を受け入れることができるサーバーを作成しようとしています(下位互換性のため)。安全でないクライアントから受信した最初の送信データがサーバーで最初の5バイト(chars)を失うことを除いて、私のコードはほぼ機能しています。より具体的には、安全でない接続で30バイトを送信する場合、サーバーがOnClientDataReceived()
関数に到達すると、行 " int iRx = nwStream.EndRead(asyn);
"、次にiRx = 25
。クライアントから送信される後続のメッセージには、期待どおりに送信されたすべてのバイト/文字が含まれます。接続の最初の仮定はSSLStream
最初の5バイトを削除している可能性があり、失敗した場合、それらの5バイトはすでにバッファーから抽出されており、使用できなくなります。サーバーがオンザフライで自動的に切り替わるようにコードを書くために私が取ることができる別のアプローチを知っている人はいますか?
私は次のことを避けようとしています:
- クライアントがプレーンテキストを使用して接続し
NetworkStream
、SSLストリームへのアップグレードを要求することを要求する - 2つの異なるポートに2つを設定する
TcpListeners
(1つは安全用、もう1つは安全でないため)
これが私のコードです:
/// 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
切り替わります。ただし、最初のメッセージに対してが呼び出されると、送信されたメッセージから最初の5文字(バイト)が欠落しますが、最初のメッセージに対してのみです。が接続されている限り、残りのすべてのメッセージは完了です。クライアントが切断してから再接続すると、最初のメッセージがクリップされ、その後のすべてのメッセージは再び正常になります。NetworkStream
TCPClient
EndRead
NetworkStream
TCPClient
これらの最初の5バイトがクリップされる原因は何ですか?どうすればそれを回避できますか?
私のプロジェクトは現在.NETv3.5を使用しています...このバージョンを維持し、回避できる場合は4.0にステップアップしないようにします。
フォローアップの質問
以下のDamienの回答では、欠落している5バイトを保持できますが、ブロックを回避するために、コード内のメソッドBeginRead
とメソッドを使用することをお勧めします。EndRead
これらをオーバーライドするときの「ベストプラクティス」を示す良いチュートリアルはありますか?より具体的には、IAsyncResult
オブジェクトの操作方法。私は、RestartableStreamバッファーに格納されているコンテンツを追加し、次に内部ストリーム(ベース)にフォールスルーして残りを取得し、トーラルを返す必要があることに気付きました。しかし、IAsyncResult
オブジェクトはカスタムクラスであるため、RestartableStreamのバフを内部ストリームのバフと組み合わせることができる一般的な方法を理解できません。ユーザーがコンテンツの保存を希望するバッファーを知るために、BeginRead()も実装する必要がありますか?もう1つの解決策は、バイトのドロップの問題はクライアントからの最初のメッセージのみにあるため(その後、それをaSSLStream
またはとして使用するかどうかがわかります)、 RestartableStreamのメソッドをNetworkStream
直接呼び出すことによってその最初のメッセージを処理することです。Read()
(一時的にコードをブロックします)その後、今後のすべてのメッセージで、非同期コールバックを使用して、現在のようにコンテンツを読み取ります。