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 @@ + +