diff --git a/README.md b/README.md
index 263aea9..5b56fbf 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,8 @@
+Moved
+=====
+
+This project is now at https://gitlab.com/derbyinsight/SocketHttpListener
+
SocketHttpListener
==================
diff --git a/SocketHttpListener.Test/HttpListenerTimeoutTest.cs b/SocketHttpListener.Test/HttpListenerTimeoutTest.cs
new file mode 100644
index 0000000..bece9cb
--- /dev/null
+++ b/SocketHttpListener.Test/HttpListenerTimeoutTest.cs
@@ -0,0 +1,495 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using HttpListener = SocketHttpListener.Net.HttpListener;
+
+
+namespace SocketHttpListener.Test
+{
+ ///
+ /// Tests for the various network timeouts.
+ ///
+ [TestClass]
+ public class HttpListenerTimeoutManagerTest
+ {
+ ///
+ /// the shortest timeout we can reasonably expect to still involve a delay.
+ ///
+ const int ShortestTime = 100;
+
+ ///
+ /// The timeout tests will reach if ShortestTime does not trigger.
+ ///
+ const int FailTestTimeout = 1000;
+
+ ///
+ /// A "very long timeout" (ms) for the HttpListener, for timeouts that we are not
+ /// exercising, and do not expect to reach even on a failing test.
+ ///
+ const int ALongTime = 5000;
+
+ const int BufferSize = 256;
+
+ HttpListener listener;
+
+ [TestInitialize]
+ public void TestInit()
+ {
+ this.listener = new HttpListener();
+
+ string url = string.Format("http://{0}", Utility.SITE_URL);
+ this.listener.Prefixes.Add(url);
+
+ this.listener.OnContext += (ctx) => {
+ Assert.Fail("Not reached");
+ };
+
+ this.listener.TimeoutManager.DrainEntityBody = TimeSpan.FromMilliseconds(ALongTime);
+ this.listener.TimeoutManager.EntityBody = TimeSpan.FromMilliseconds(ALongTime);
+ this.listener.TimeoutManager.HeaderWait = TimeSpan.FromMilliseconds(ALongTime);
+ this.listener.TimeoutManager.IdleConnection = TimeSpan.FromMilliseconds(ALongTime);
+ }
+
+ [TestCleanup]
+ public void TestCleanup()
+ {
+ if (null != this.listener)
+ {
+ this.listener.Close();
+ this.listener = null;
+ }
+ }
+
+
+ [TestMethod]
+ public async Task TestHttpKeepAliveTimeout()
+ {
+ // Exercise the IdleConnection timeout. (keepalive)
+
+ this.listener.TimeoutManager.IdleConnection = TimeSpan.FromMilliseconds(100);
+ this.listener.Start();
+
+ using (Socket socket = this.GetConnectedSocket())
+ {
+ Assert.IsTrue(this.IsSocketConnected(socket), "Server should be waiting for us to send a header.");
+ await Task.Delay(FailTestTimeout);
+ Assert.IsFalse(this.IsSocketConnected(socket), "Server should have given up by now");
+ }
+
+ // Set longer timeout
+ this.listener.Stop();
+ this.listener.TimeoutManager.IdleConnection = TimeSpan.FromMilliseconds(ALongTime);
+ this.listener.Start();
+
+ using (Socket socket = this.GetConnectedSocket())
+ {
+ Assert.IsTrue(this.IsSocketConnected(socket), "Server should be waiting for us to send a header.");
+ await Task.Delay(FailTestTimeout);
+ Assert.IsTrue(this.IsSocketConnected(socket), "Server should still be waiting");
+ }
+ }
+
+ const string IncompleteHttpHeader =
+ "GET /something HTTP/1.1\n" +
+ "Host: 127.0.0.1\n" +
+ "Accept: tex"
+ ;
+
+
+ const string SimpleHttpGet =
+ "GET " + Utility.SITE_PREFIX + " HTTP/1.1\n" +
+ "Host: " + Utility.SITE_HOSTNAME + "\n" +
+ "Accept: text/xml\n" +
+ "Connection: keep-alive\n" +
+ "Keep-Alive: 300000\n" +
+ "Content-Type: text/xml\n" +
+ "Content-Length: 0\n" +
+ "\n"
+ ;
+ const string IncompleteHttpPost =
+ "POST " + Utility.SITE_PREFIX + " HTTP/1.1\n" +
+ "Host: " + Utility.SITE_HOSTNAME + "\n" +
+ "Accept: text/xml\n" +
+ "Connection: keep-alive\n" +
+ "Keep-Alive: 300000\n" +
+ "Content-Type: text/xml\n" +
+ "Content-Length: 9995\n" +
+ "\n" +
+ " { socket.Send(buffer); });
+ Assert.IsTrue(this.IsSocketConnected(socket), "Server should be waiting for the rest");
+ await Task.Delay(FailTestTimeout);
+ Assert.IsTrue(this.IsSocketConnected(socket), "Server should still be waiting.");
+ }
+ }
+
+ [TestMethod]
+ public async Task TestHttpReadBodySyncTimeout()
+ {
+ this.listener.TimeoutManager.HeaderWait = TimeSpan.FromMilliseconds(100);
+ this.listener.TimeoutManager.EntityBody = TimeSpan.FromMilliseconds(100);
+
+ bool gotReadTimeout = false;
+ this.listener.OnContext = (ctx) => {
+ using (TextReader reader = new StreamReader(ctx.Request.InputStream))
+ {
+ Task readToEnd = Task.Run( () => {
+ reader.ReadToEnd(); // Will block until the socket times out.
+ });
+
+ try
+ {
+ Assert.IsFalse(readToEnd.Wait(FailTestTimeout), "gave up waiting for the socket timeout");
+ }
+ catch (AggregateException aex)
+ {
+ Assert.IsTrue(aex.InnerException is IOException);
+ gotReadTimeout = true;
+ }
+ }
+ };
+
+ this.listener.Start();
+
+ byte [] buffer = Encoding.UTF8.GetBytes(IncompleteHttpPost);
+ using (Socket socket = this.GetConnectedSocket())
+ {
+ // Send an incomplete header.
+ this.AwaitWithTimeout(() => { socket.Send(buffer); });
+ Assert.IsTrue(this.IsSocketConnected(socket), "Server should be waiting for the rest");
+ await Task.Delay(FailTestTimeout); // Wait for socket to time out and the handler to fire
+ Assert.IsTrue(gotReadTimeout, "The context handler should have timed out");
+ }
+
+ this.listener.Stop();
+ this.listener.TimeoutManager.EntityBody = TimeSpan.FromMilliseconds(ALongTime);
+ this.listener.Start();
+
+ gotReadTimeout = false;
+ using (Socket socket = this.GetConnectedSocket())
+ {
+ // Send an incomplete header.
+ this.AwaitWithTimeout(() => { socket.Send(buffer); });
+ await Task.Delay(FailTestTimeout); // Wait for socket to time out and the handler to fire
+ Assert.IsFalse(gotReadTimeout, "The context handler should still be waiting");
+ }
+ }
+
+ const string ResponseBody =
+ "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ + "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
+ ;
+
+ ///
+ /// Exercise the Write function's synchronous timeout. ResponseStream.Close()
+ /// also makes internal calls to synchronous Write() via InternalWrite, exercised
+ /// thusly.
+ ///
+ [TestMethod]
+ public async Task Test_ResponseStream_WriteSynchronous()
+ {
+ // Notes:
+ // SendHeaders() is triggered by ResponseStream.GetHeaders(), which can be triggered
+ // by Write, BeginWrite, and Close. It may not be possible to exercise the timeout
+ // in SendHeaders, because of output buffering.
+
+ this.listener.TimeoutManager.DrainEntityBody = TimeSpan.FromMilliseconds(100);
+
+ bool gotTimeout = false;
+ this.listener.OnContext = (ctx) => {
+ using (TextWriter writer = new StreamWriter(ctx.Response.OutputStream, Encoding.UTF8, BufferSize))
+ {
+ Task writerTask = Task.Run( () => {
+ // Fill the buffers with junk!
+ while (true)
+ writer.Write(ResponseBody);
+ });
+
+ try
+ {
+ writerTask.Wait(FailTestTimeout); // will throw on socket timeout
+ }
+ catch (AggregateException aex)
+ {
+ Assert.IsTrue(aex.InnerException is IOException);
+ gotTimeout = true;
+ }
+ }
+ ctx.Response.Close();
+ };
+
+ this.listener.Start();
+
+ byte [] buffer = Encoding.UTF8.GetBytes(IncompleteHttpPost);
+ gotTimeout = false; // not yet
+ using (Socket socket = GetConnectedSocket())
+ {
+ this.AwaitWithTimeout(() => { socket.Send(buffer); });
+ await Task.Delay(FailTestTimeout); // Wait for socket to time out and the handler to fire
+ Assert.IsTrue(gotTimeout);
+ // Can't use this.IsSocketConnected(), because the test needs the reader to stall.
+ }
+
+ // * It won't time out, when the timeout is "long"
+ this.listener.Stop();
+ this.listener.TimeoutManager.DrainEntityBody = TimeSpan.FromMilliseconds(ALongTime);
+ this.listener.Start();
+
+ gotTimeout = false;
+ using (Socket socket = GetConnectedSocket())
+ {
+ this.AwaitWithTimeout(() => { socket.Send(buffer); });
+ await Task.Delay(FailTestTimeout);
+ Assert.IsFalse(gotTimeout);
+ }
+ }
+
+ ///
+ /// Make sure async writes time out properly.
+ ///
+ [TestMethod]
+ public async Task Test_ResponseStream_WriteAsync()
+ {
+ this.listener.TimeoutManager.DrainEntityBody = TimeSpan.FromMilliseconds(100);
+
+ bool gotTimeout = false;
+ this.listener.OnContext = (ctx) => {
+ using (TextWriter writer = new StreamWriter(ctx.Response.OutputStream))
+ {
+ Task writerTask = Task.Run(async () => {
+ // Fill the buffers with junk...asynchronously!
+ while (true) {
+ await writer.WriteAsync("12345678901234567890123456789012345678901234567890123456789012345678901234567890\n");
+ }
+ });
+
+ try
+ {
+ writerTask.Wait(FailTestTimeout);
+ Assert.Fail("Not reached: should have thrown exception.");
+ }
+ catch (AggregateException aex)
+ {
+ Assert.IsTrue(aex.InnerException is IOException);
+ gotTimeout = true;
+ }
+ }
+ ctx.Response.Close();
+ };
+
+ this.listener.Start();
+
+ byte [] buffer = Encoding.UTF8.GetBytes(IncompleteHttpPost);
+ byte [] readBuffer = new byte[1000];
+ using (Socket socket = GetConnectedSocket())
+ {
+ this.AwaitWithTimeout(() => { socket.Send(buffer); });
+ await Task.Delay(FailTestTimeout); // Wait for socket to time out and the handler to fire
+
+ // Get the first few bytes to get the flow started, then stall.
+ this.AwaitWithTimeout(() => { socket.Receive(readBuffer); }, FailTestTimeout);
+ await Task.Delay(FailTestTimeout); // Wait for socket to time out and the handler to fire
+ Assert.IsTrue(gotTimeout);
+ }
+
+ // * Fail test
+
+ this.listener.Stop();
+ this.listener.TimeoutManager.DrainEntityBody = TimeSpan.FromMilliseconds(ALongTime);
+ this.listener.Start();
+
+ gotTimeout = false;
+ using (Socket socket = GetConnectedSocket())
+ {
+ this.AwaitWithTimeout(() => { socket.Send(buffer); });
+ await Task.Delay(FailTestTimeout); // Wait for socket to time out and the handler to fire
+
+ // Get the first few bytes to get the flow started, then stall.
+ this.AwaitWithTimeout(() => { socket.Receive(readBuffer); }, FailTestTimeout);
+ await Task.Delay(FailTestTimeout); // Wait for socket to time out and the handler to fire
+ Assert.IsFalse(gotTimeout);
+ }
+ }
+
+ ///
+ /// test timeouts in HttpListenerRequest.FlushInput() triggered by HttpListenerResponse.Close().
+ ///
+ [TestMethod]
+ public async Task Test_HttpListenerRequest_FlushInput()
+ {
+ this.listener.TimeoutManager.EntityBody = TimeSpan.FromMilliseconds(100); // The relevant timeout
+
+ this.listener.OnContext = (ctx) => {
+
+ // Do not close ctx.Request.InputStream.
+ // If we close the input stream, FlushInput will not be able
+ // to read from the input, and we will not be testing its timeouts.
+
+ ctx.Response.Close(); // Closing the response trigers FlushInput()
+ };
+ this.listener.Start();
+
+ // With a 100ms timeout, the socket should time out when it can not read the rest of the request body
+
+ byte [] buffer = Encoding.UTF8.GetBytes(IncompleteHttpPost);
+ using (Socket socket = GetConnectedSocket())
+ {
+ this.AwaitWithTimeout(() => {
+ socket.Send(buffer);
+ });
+
+ // Give the OnContext handler time to trigger the network flush and time out.
+ await(Task.Delay(FailTestTimeout));
+ Assert.IsFalse(this.IsSocketConnected(socket), "Server should have closed the connection by now");
+ }
+
+
+ // Test failure: "I want to see a negative before I provide you with a positive."
+ // With a LONG timeout, the listener should hang.
+
+ this.listener.Stop();
+ this.listener.TimeoutManager.EntityBody = TimeSpan.FromMilliseconds(ALongTime);
+ this.listener.Start();
+
+ using (Socket socket = GetConnectedSocket())
+ {
+ this.AwaitWithTimeout(() => {
+ socket.Send(buffer);
+ });
+
+ // Give the OnContext handler time to trigger the network flush and time out.
+ await(Task.Delay(FailTestTimeout));
+ Assert.IsTrue(this.IsSocketConnected(socket), "Server should still be stuck in FlushInput()");
+ }
+ }
+
+ bool IsSocketConnected(Socket socket)
+ {
+ // This will detect if the socket was closed gracefully, but not a
+ // hard network fault/pulled cable. Should work for testing.
+
+ // The test only works if the read buffer has been drained.
+ // In this test, this side effect is acceptable, but beware if copying
+ // this code for use elsewhere.
+
+ byte [] buffer = new byte[1024];
+ while (socket.Available > 0) {
+ socket.Receive(buffer);
+ }
+
+ bool part1 = socket.Poll(FailTestTimeout, SelectMode.SelectRead);
+ bool part2 = (socket.Available == 0);
+ if (part1 && part2)
+ return false;
+ else
+ return true;
+ }
+
+ ///
+ /// Return a socket connected to the test listener.
+ ///
+ ///
+ Socket GetConnectedSocket()
+ {
+ Socket socket = new Socket(SocketType.Stream, ProtocolType.IP);
+ try
+ {
+ // Establish a no-linger, small-buffers socket connection for simulating/detecting
+ // stalled network conditions.
+
+ socket.LingerState = new LingerOption(true, 0); // disconnect immediately please
+ socket.NoDelay = true;
+ socket.ReceiveBufferSize = BufferSize;
+ socket.SendBufferSize = BufferSize;
+ this.AwaitWithTimeout(() => {
+ socket.Connect(Utility.SITE_HOSTNAME, Utility.SITE_PORT);
+ });
+
+ Assert.IsTrue(this.IsSocketConnected(socket), "Test IsSocketConnected()");
+ return socket;
+ }
+ catch (Exception)
+ {
+ socket.Dispose();
+ throw;
+ }
+ }
+
+ void AwaitWithTimeout(Action action)
+ {
+ this.AwaitWithTimeout(Task.Run(action), ALongTime);
+ }
+
+ void AwaitWithTimeout(Action action, int timeoutMs)
+ {
+ this.AwaitWithTimeout(Task.Run(action), timeoutMs);
+ }
+
+ ///
+ /// Simulate "waiting forever", using a timeout that is longer than the
+ /// longest timeout in the HttpListener.
+ ///
+ void AwaitWithTimeout(Task task)
+ {
+ this.AwaitWithTimeout(task, ALongTime);
+ }
+
+ ///
+ /// Throws TimeoutException if timeout is reached
+ ///
+ void AwaitWithTimeout(Task task, int timeoutMs)
+ {
+ try
+ {
+ if (!task.Wait(timeoutMs))
+ throw new TimeoutException();
+ }
+ catch (AggregateException aex)
+ {
+ if (null != aex.InnerException)
+ throw aex.InnerException;
+
+ throw;
+ }
+ }
+ }
+}
diff --git a/SocketHttpListener.Test/ResumableTimerTest.cs b/SocketHttpListener.Test/ResumableTimerTest.cs
new file mode 100644
index 0000000..34852a7
--- /dev/null
+++ b/SocketHttpListener.Test/ResumableTimerTest.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+using ResumableTimer = SocketHttpListener.Net.ResumableTimer;
+
+namespace SocketHttpListener.Test
+{
+ [TestClass]
+ public class ResumableTimerTest
+ {
+ object timeoutData;
+
+ [TestInitialize]
+ public void TestInit()
+ {
+ this.timeoutData = 1;
+ }
+
+ [TestCleanup]
+ public void TestCleanup()
+ {
+ }
+
+ [TestMethod]
+ public async Task TestGenericTimeout()
+ {
+ ResumableTimer rt = new ResumableTimer(TimeoutCallback, 2);
+
+ rt.Start(TimeSpan.FromMilliseconds(100));
+ Assert.AreEqual(this.timeoutData, 1);
+ await Task.Delay(250);
+ Assert.AreEqual(this.timeoutData, 2);
+ }
+
+ [TestMethod]
+ public async Task TestNoTimeoutAndReset()
+ {
+ ResumableTimer rt = new ResumableTimer(TimeoutCallback, 2);
+
+ rt.Start(TimeSpan.FromHours(500));
+ await Task.Delay(250);
+ Assert.AreEqual(this.timeoutData, 1);
+
+ rt.Start(TimeSpan.FromMilliseconds(100));
+ await Task.Delay(250);
+ Assert.AreEqual(this.timeoutData, 2);
+ }
+
+ [TestMethod]
+ public async Task TestStopAndResume()
+ {
+ ResumableTimer rt = new ResumableTimer(TimeoutCallback, 2);
+
+ rt.Start(TimeSpan.FromMilliseconds(150));
+ await Task.Delay(10);
+ Assert.AreEqual(this.timeoutData, 1);
+
+ rt.Stop();
+ rt.Resume();
+ await Task.Delay(10);
+ Assert.AreEqual(this.timeoutData, 1, "How long did stop/resume take?");
+
+ rt.Stop();
+ await Task.Delay(250);
+ Assert.AreEqual(this.timeoutData, 1, "Ensure that Stop/Resume work without timeout");
+
+ rt.Resume();
+ await Task.Delay(10);
+ Assert.AreEqual(this.timeoutData, 2, "Ensure that Resume worked");
+ }
+
+ void TimeoutCallback(object data)
+ {
+ timeoutData = data;
+ }
+ }
+}
diff --git a/SocketHttpListener.Test/SocketHttpListener.Test.csproj b/SocketHttpListener.Test/SocketHttpListener.Test.csproj
index 2f08964..560b72b 100644
--- a/SocketHttpListener.Test/SocketHttpListener.Test.csproj
+++ b/SocketHttpListener.Test/SocketHttpListener.Test.csproj
@@ -63,6 +63,8 @@
+
+
diff --git a/SocketHttpListener.Test/Utility.cs b/SocketHttpListener.Test/Utility.cs
index 5f4706b..5a64688 100644
--- a/SocketHttpListener.Test/Utility.cs
+++ b/SocketHttpListener.Test/Utility.cs
@@ -10,6 +10,9 @@ namespace SocketHttpListener.Test
{
internal static class Utility
{
+ internal const string SITE_HOSTNAME = "localhost";
+ internal const int SITE_PORT = 12345;
+ internal const string SITE_PREFIX = "/Testing";
internal const string SITE_URL = "localhost:12345/Testing/";
internal const string TEXT_TO_WRITE = "TESTING12345";
diff --git a/SocketHttpListener/Net/HttpConnection.cs b/SocketHttpListener/Net/HttpConnection.cs
index 861b027..91c5735 100644
--- a/SocketHttpListener/Net/HttpConnection.cs
+++ b/SocketHttpListener/Net/HttpConnection.cs
@@ -30,8 +30,7 @@ sealed class HttpConnection
int reuses;
bool context_bound;
bool secure;
- int s_timeout = 300000; // 90k ms for first request, 15k ms from then on
- Timer timer;
+ ResumableTimer timer;
IPEndPoint local_ep;
HttpListener last_listener;
int[] client_cert_errors;
@@ -47,6 +46,7 @@ public HttpConnection(ILogger logger, Socket sock, EndPointListener epl, bool se
this.epl = epl;
this.secure = secure;
this.cert = cert;
+ this.SetSocketTimeout(sock);
if (secure == false)
{
stream = new NetworkStream(sock, false);
@@ -70,10 +70,30 @@ public HttpConnection(ILogger logger, Socket sock, EndPointListener epl, bool se
ssl_stream.AuthenticateAsServer(cert);
stream = ssl_stream;
}
- timer = new Timer(OnTimeout, null, Timeout.Infinite, Timeout.Infinite);
+ timer = new ResumableTimer(OnTimeout);
Init();
}
+ void SetSocketTimeout(Socket sock)
+ {
+ // Socket timeout should be >= the largest applicable http listener timeout, and
+ // Certainly > 0.
+ HttpListenerTimeoutManager mgr = this.epl.Listener.TimeoutManager;
+
+ TimeSpan readTimeout = TimeSpan.FromMilliseconds(50);
+ if (mgr.EntityBody > readTimeout)
+ readTimeout = mgr.EntityBody;
+ if (mgr.HeaderWait > readTimeout)
+ readTimeout = mgr.HeaderWait;
+
+ TimeSpan writeTimeout = TimeSpan.FromMilliseconds(50);
+ if (mgr.DrainEntityBody > writeTimeout)
+ writeTimeout = mgr.DrainEntityBody;
+
+ sock.ReceiveTimeout = (int)readTimeout.TotalMilliseconds;
+ sock.SendTimeout = (int)writeTimeout.TotalMilliseconds;
+ }
+
public Stream Stream
{
get
@@ -159,12 +179,13 @@ public void BeginReadRequest()
{
//if (reuses == 1)
// s_timeout = 15000;
- timer.Change(s_timeout, Timeout.Infinite);
+
+ timer.Start(this.epl.Listener.TimeoutManager.IdleConnection);
stream.BeginRead(buffer, 0, BufferSize, onread_cb, this);
}
catch
{
- timer.Change(Timeout.Infinite, Timeout.Infinite);
+ timer.Stop();
CloseSocket();
Unbind();
}
@@ -213,7 +234,7 @@ static void OnRead(IAsyncResult ares)
void OnReadInternal(IAsyncResult ares)
{
- timer.Change(Timeout.Infinite, Timeout.Infinite);
+ timer.Stop();
int nread = -1;
try
{
@@ -273,6 +294,7 @@ void OnReadInternal(IAsyncResult ares)
}
try
{
+ timer.Start(this.epl.Listener.TimeoutManager.HeaderWait);
stream.BeginRead(buffer, 0, BufferSize, onread_cb, this);
}
catch (IOException ex)
@@ -511,7 +533,7 @@ internal void Close(bool force_close)
}
*/
- if (!force_close && context.Request.FlushInput())
+ if (!force_close && context.Request.FlushInput(this.epl.Listener.TimeoutManager.EntityBody))
{
if (chunked && context.Response.ForceCloseChunked == false)
{
@@ -523,6 +545,7 @@ internal void Close(bool force_close)
return;
}
+ // BUG: isn't this exactly the same code that is in the if() block above?
reuses++;
Unbind();
Init();
diff --git a/SocketHttpListener/Net/HttpListener.cs b/SocketHttpListener/Net/HttpListener.cs
index dcf0cb1..608b04a 100644
--- a/SocketHttpListener/Net/HttpListener.cs
+++ b/SocketHttpListener/Net/HttpListener.cs
@@ -84,6 +84,15 @@ public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate
}
}
+ HttpListenerTimeoutManager timeoutManager = new HttpListenerTimeoutManager();
+ public HttpListenerTimeoutManager TimeoutManager
+ {
+ get
+ {
+ return this.timeoutManager;
+ }
+ }
+
public bool IgnoreWriteExceptions
{
get { return ignore_write_exceptions; }
diff --git a/SocketHttpListener/Net/HttpListenerRequest.cs b/SocketHttpListener/Net/HttpListenerRequest.cs
index 483beae..1a3fb7b 100644
--- a/SocketHttpListener/Net/HttpListenerRequest.cs
+++ b/SocketHttpListener/Net/HttpListenerRequest.cs
@@ -151,7 +151,7 @@ internal void FinishInitialization()
if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out url))
{
context.ErrorMessage = WebUtility.HtmlEncode("Invalid url: " + base_uri + path);
- return; return;
+ return;
}
CreateQueryString(url.Query);
@@ -383,7 +383,7 @@ internal void AddHeader(string header)
}
// returns true is the stream could be reused.
- internal bool FlushInput()
+ internal bool FlushInput(TimeSpan timeout)
{
if (!HasEntityBody)
return true;
@@ -395,13 +395,14 @@ internal bool FlushInput()
byte[] bytes = new byte[length];
while (true)
{
- // TODO: test if MS has a timeout when doing this
try
{
- IAsyncResult ares = InputStream.BeginRead(bytes, 0, length, null, null);
- if (!ares.IsCompleted && !ares.AsyncWaitHandle.WaitOne(1000))
+ Task readTask = InputStream.ReadAsync(bytes, 0, length);
+ if (!readTask.Wait(timeout))
return false;
- if (InputStream.EndRead(ares) <= 0)
+
+ int bytesRead = readTask.Result;
+ if (bytesRead <= 0)
return true;
}
catch (ObjectDisposedException e)
diff --git a/SocketHttpListener/Net/HttpListenerTimeoutManager.cs b/SocketHttpListener/Net/HttpListenerTimeoutManager.cs
new file mode 100644
index 0000000..e04921d
--- /dev/null
+++ b/SocketHttpListener/Net/HttpListenerTimeoutManager.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SocketHttpListener.Net
+{
+ ///
+ /// Timeouts.
+ ///
+ public class HttpListenerTimeoutManager
+ {
+ private TimeSpan idleConnection = TimeSpan.FromMinutes(5);
+
+ ///
+ /// Time the listener will wait for the next HTTP request.
+ ///
+ /// Defaults to 5 minutes for backward compatibility.
+ ///
+ /// This is the default/maximum timeout that will be used while waiting for
+ /// a KeepAlive session.
+ ///
+ public TimeSpan IdleConnection
+ {
+ get
+ {
+ return this.idleConnection;
+ }
+ set
+ {
+ this.idleConnection = value;
+ }
+ }
+
+ private TimeSpan headerWait = TimeSpan.FromMinutes(5);
+ /////
+ ///// Maximum time the listener will spend reading an HTTP request's headers.
+ /////
+ ///// The network read timeout used when http headers have started
+ ///// to arrive but not finished.
+ /////
+ public TimeSpan HeaderWait
+ {
+ get
+ {
+ return this.headerWait;
+ }
+ set
+ {
+ this.headerWait = value;
+ }
+ }
+
+ private TimeSpan entityBody = TimeSpan.FromMinutes(5);
+ ///
+ /// The read timeout when reading the body. Set for synchronous IO.
+ ///
+ /// Not enforced. If the OnContext handler uses async IO, it will need to
+ /// handle timeouts man ually.
+ ///
+ /// Defaults to 5 minutes.
+ ///
+ public TimeSpan EntityBody
+ {
+ get
+ {
+ return this.entityBody;
+ }
+ set
+ {
+ this.entityBody = value;
+ }
+ }
+
+ private TimeSpan drainEntityBody = TimeSpan.FromMinutes(5);
+ ///
+ /// The write timeout, used during the body write. Set for synchronous IO.
+ ///
+ /// If the OnContext handler uses async IO, it will need to handle timeouts
+ /// manually.
+ ///
+ /// This timeout is used for all HTTP writes, including headers, because the
+ /// HttpListenerTimeoutManager spec does not have a separate timeout field for
+ /// this.
+ ///
+ public TimeSpan DrainEntityBody
+ {
+ get
+ {
+ return this.drainEntityBody;
+ }
+ set
+ {
+ this.drainEntityBody = value;
+ }
+ }
+ }
+}
diff --git a/SocketHttpListener/Net/ResponseStream.cs b/SocketHttpListener/Net/ResponseStream.cs
index 8bb9c24..bce6b20 100644
--- a/SocketHttpListener/Net/ResponseStream.cs
+++ b/SocketHttpListener/Net/ResponseStream.cs
@@ -173,6 +173,7 @@ public override void Write(byte[] buffer, int offset, int count)
InternalWrite(crlf, 0, 2);
}
+ // TODO: sending headers is not async, BeginWrite can block until socket timeout
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
if (disposed)
diff --git a/SocketHttpListener/Net/ResumableTimer.cs b/SocketHttpListener/Net/ResumableTimer.cs
new file mode 100644
index 0000000..090d988
--- /dev/null
+++ b/SocketHttpListener/Net/ResumableTimer.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Threading;
+
+namespace SocketHttpListener.Net
+{
+ ///
+ /// A Timer that can be stopped and restarted, for timing the total duration
+ /// of async operations.
+ ///
+ public class ResumableTimer
+ {
+ readonly Timer timer;
+ DateTime endTime = DateTime.Now;
+
+ public ResumableTimer(TimerCallback timeoutCallback)
+ : this(timeoutCallback, null)
+ {
+ }
+
+ public ResumableTimer(TimerCallback timeoutCallback, object data)
+ {
+ timer = new Timer(timeoutCallback, data, Timeout.Infinite, Timeout.Infinite);
+ }
+
+ public void Start(TimeSpan newTimeout)
+ {
+ this.endTime = DateTime.Now + newTimeout;
+ this.timer.Change(newTimeout, Timeout.InfiniteTimeSpan);
+ }
+
+ public void Stop()
+ {
+ this.timer.Change(Timeout.Infinite, Timeout.Infinite);
+ }
+
+ ///
+ /// Continue running a paused timer.
+ ///
+ public void Resume()
+ {
+ TimeSpan timeLeft = endTime - DateTime.Now;
+ if (timeLeft < TimeSpan.Zero)
+ timeLeft = TimeSpan.Zero;
+
+ this.Start(timeLeft);
+ }
+ }
+}
diff --git a/SocketHttpListener/SocketHttpListener.csproj b/SocketHttpListener/SocketHttpListener.csproj
index 0e697a0..a99dfdd 100644
--- a/SocketHttpListener/SocketHttpListener.csproj
+++ b/SocketHttpListener/SocketHttpListener.csproj
@@ -67,12 +67,14 @@
+
+