A brief synopsis of the situation:
I have a service that takes information in and sends replies out over Sockets. The connections are unsecured. I want to setup another service that can provide TLS to these connections - this new service would provide a single port and distribute the connections based on the client certificate provided. I don't want to use stunnel for a couple reasons, one being that it would require one forwarding port per receiving port.
The solution I'm currently trying to implement:
Essentially, I'm trying to couple an SslStream (incoming) with a NetworkStream (outgoing - could be a Socket, but I put it into a NetworkStream to match the incoming) and have the read/write operations linked for the two. This link would provide the flow between the client (over SSL/TLS) and the service (over an unsecured connection).
Here's the class I came up with to link these Streams:
public class StreamConnector
{
public StreamConnector(Stream s1, Stream s2)
{
StreamConnectorState state1 = new StreamConnectorState(s1, s2);
StreamConnectorState state2 = new StreamConnectorState(s2, s1);
s1.BeginRead(state1.Buffer, 0, state1.Buffer.Length, new AsyncCallback(ReadCallback), state1);
s2.BeginRead(state2.Buffer, 0, state2.Buffer.Length, new AsyncCallback(ReadCallback), state2);
}
private void ReadCallback(IAsyncResult result)
{
// Get state object.
StreamConnectorState state = (StreamConnectorState)result.AsyncState;
// Finish reading data.
int length = state.InStream.EndRead(result);
// Write data.
state.OutStream.Write(state.Buffer, 0, length);
// Wait for new data.
state.InStream.BeginRead(state.Buffer, 0, state.Buffer.Length, new AsyncCallback(ReadCallback), state);
}
}
public class StreamConnectorState
{
private const int BYTE_ARRAY_SIZE = 4096;
public byte[] Buffer { get; set; }
public Stream InStream { get; set; }
public Stream OutStream { get; set; }
public StreamConnectorState(Stream inStream, Stream outStream)
{
Buffer = new byte[BYTE_ARRAY_SIZE];
InStream = inStream;
OutStream = outStream;
}
}
The problem:
When the client is done sending information and disposes of the SslStream, the server doesn't have any sort of indication of whether or not this has happened. This StreamConnector class happily keeps running into eternity without throwing any sort of error, and I can't find any indicator that it should stop. (There is, of course, the fact that I get 0 length every time in ReadCallback, but I need to be able to provide long-running connections, so this isn't a good way to judge.)
Another potential issue is that the ReadCallback gets called even if no data is available. Not sure if that would be different if I were using a Socket directly instead of a stream, but it seems inefficient to keep running that code over and over again.
My questions:
1) Is there a way to tell if a Stream has been closed from the client side?
2) Is there a better way to do what I am trying to do?
2a) Is there a more efficient way to run the asynchronous read/write loop?
EDIT: Thanks, Robert. Turns out the loop kept getting called because I wasn't closing the Streams (due to not knowing how to tell when the Streams needed to be closed). I'm including the full code solution in case someone else runs into this issue:
/// <summary>
/// Connects the read/write operations of two provided streams
/// so long as both of the streams remain open.
/// Disposes of both streams when either of them disconnect.
/// </summary>
public class StreamConnector
{
public StreamConnector(Stream s1, Stream s2)
{
StreamConnectorState state1 = new StreamConnectorState(s1, s2);
StreamConnectorState state2 = new StreamConnectorState(s2, s1);
s1.BeginRead(state1.Buffer, 0, state1.Buffer.Length, new AsyncCallback(ReadCallback), state1);
s2.BeginRead(state2.Buffer, 0, state2.Buffer.Length, new AsyncCallback(ReadCallback), state2);
}
private void ReadCallback(IAsyncResult result)
{
// Get state object.
StreamConnectorState state = (StreamConnectorState)result.AsyncState;
// Check to make sure Streams are still connected before processing.
if (state.InStream.IsConnected() && state.OutStream.IsConnected())
{
// Finish reading data.
int length = state.InStream.EndRead(result);
// Write data.
state.OutStream.Write(state.Buffer, 0, length);
// Wait for new data.
state.InStream.BeginRead(state.Buffer, 0, state.Buffer.Length, new AsyncCallback(ReadCallback), state);
}
else
{
// Dispose of both streams if either of them is no longer connected.
state.InStream.Dispose();
state.OutStream.Dispose();
}
}
}
public class StreamConnectorState
{
private const int BYTE_ARRAY_SIZE = 4096;
public byte[] Buffer { get; set; }
public Stream InStream { get; set; }
public Stream OutStream { get; set; }
public StreamConnectorState(Stream inStream, Stream outStream)
{
Buffer = new byte[BYTE_ARRAY_SIZE];
InStream = inStream;
OutStream = outStream;
}
}
public static class StreamExtensions
{
private static readonly byte[] POLLING_BYTE_ARRAY = new byte[0];
public static bool IsConnected(this Stream stream)
{
try
{
// Twice because the first time will return without issue but
// cause the Stream to become closed (if the Stream is actually
// closed.)
stream.Write(POLLING_BYTE_ARRAY, 0, POLLING_BYTE_ARRAY.Length);
stream.Write(POLLING_BYTE_ARRAY, 0, POLLING_BYTE_ARRAY.Length);
return true;
}
catch (ObjectDisposedException)
{
// Since we're disposing of both Streams at the same time, one
// of the streams will be checked after it is disposed.
return false;
}
catch (IOException)
{
// This will be thrown on the second stream.Write when the Stream
// is closed on the client side.
return false;
}
}
}
You have to attempt to read or write to a socket -- or anything based on it -- to detect a disconnect.
Attempting to write will throw an exception/return an error (depending on your language's paradigm) or possibly just write 0 bytes. Attempting to read will either throw an exception/return an error (again depending on your language's paradigm) or return null
.
It's worth noting that if you're using a select-based server model, the disconnected socket shows up -- i.e. returns select -- as readable when it disconnects, then you attempt to read from it and get the error or null
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With