From c5bd6c73271ff15a0563a042d3166469a9a5c040 Mon Sep 17 00:00:00 2001 From: mzabani Date: Thu, 21 Nov 2013 10:39:28 -0200 Subject: [PATCH] - Header dictionary has a class for itself now, and some tests - Some additions to OwinContext - Much improved StatsLogger and StatsPageMiddleware (still public) - ShuntMiddleware added to map requests to a different IAppBuilder based on request path matching - License added (BSD License) --- COPYING | 26 ++++ Fos.Tests/Fos.Tests.csproj | 1 + Fos.Tests/FragmentedResponseStreamTests.cs | 2 +- Fos.Tests/Logger/AbruptConnectionClosing.cs | 36 ++++- Fos.Tests/Logger/NormalConnectionClosing.cs | 14 +- Fos.Tests/OwinContext.cs | 6 +- Fos.Tests/ResponseAndRequestHeaders.cs | 37 +++++ Fos/AssemblyInfo.cs | 2 +- Fos/Fos.csproj | 4 + Fos/FosSelfHost.cs | 2 +- Fos/Listener/SocketListener.cs | 15 +- Fos/Logging/ApplicationError.cs | 34 +++++ Fos/Logging/StatsLogger.cs | 112 +++++++++----- Fos/Logging/StatsPageMiddleware.cs | 118 +++++++++++++++ Fos/Middleware/PageNotFoundMiddleware.cs | 11 +- Fos/Middleware/ShuntMiddleware.cs | 76 ++++++++++ Fos/Owin/FCgiAppBuilder.cs | 8 +- Fos/Owin/HeaderDictionary.cs | 156 ++++++++++++++++++++ Fos/Owin/OwinContext.cs | 10 +- 19 files changed, 595 insertions(+), 75 deletions(-) create mode 100644 COPYING create mode 100644 Fos.Tests/ResponseAndRequestHeaders.cs create mode 100644 Fos/Logging/ApplicationError.cs create mode 100644 Fos/Logging/StatsPageMiddleware.cs create mode 100644 Fos/Middleware/ShuntMiddleware.cs create mode 100644 Fos/Owin/HeaderDictionary.cs diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..e7e99fc --- /dev/null +++ b/COPYING @@ -0,0 +1,26 @@ +Copyright (c) 2013, Marcelo Zabani +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation are those +of the authors and should not be interpreted as representing official policies, +either expressed or implied, of the FreeBSD Project. \ No newline at end of file diff --git a/Fos.Tests/Fos.Tests.csproj b/Fos.Tests/Fos.Tests.csproj index cc5de5b..3d82a3b 100644 --- a/Fos.Tests/Fos.Tests.csproj +++ b/Fos.Tests/Fos.Tests.csproj @@ -53,6 +53,7 @@ + diff --git a/Fos.Tests/FragmentedResponseStreamTests.cs b/Fos.Tests/FragmentedResponseStreamTests.cs index ecd2680..53f784f 100644 --- a/Fos.Tests/FragmentedResponseStreamTests.cs +++ b/Fos.Tests/FragmentedResponseStreamTests.cs @@ -35,7 +35,7 @@ public void OnFirstWriteAndOnStreamFillEvents() int numFilledStreams = 5; int chunkSize = 65535 * numFilledStreams + 1; byte[] hugeChunk = new byte[chunkSize]; - Assert.AreEqual(null, lastFilledStream); + Assert.IsNull(lastFilledStream); Assert.AreEqual(0, numStreamFills); s.Write(hugeChunk, 0, chunkSize); Assert.AreEqual(numFilledStreams, numStreamFills); diff --git a/Fos.Tests/Logger/AbruptConnectionClosing.cs b/Fos.Tests/Logger/AbruptConnectionClosing.cs index 83aa0fd..300b082 100644 --- a/Fos.Tests/Logger/AbruptConnectionClosing.cs +++ b/Fos.Tests/Logger/AbruptConnectionClosing.cs @@ -1,6 +1,7 @@ using System; using Owin; using NUnit.Framework; +using FastCgiNet; using Fos; using Fos.Logging; using System.Net.Sockets; @@ -26,18 +27,43 @@ public void CloseConnectionAbruptlyBeforeSendingAnyRecord() sock.Close(); } + //TODO: Can't we do better than this? System.Threading.Thread.Sleep(10); } - Assert.AreEqual(true, logger.ConnectionWasReceived); - Assert.AreEqual(false, logger.ConnectionClosedNormally); - Assert.AreEqual(null, logger.RequestInfo); - Assert.AreEqual(true, logger.ConnectionClosedAbruptlyWithoutAnyRequestInfo); + Assert.IsTrue(logger.ConnectionWasReceived); + Assert.IsFalse(logger.ConnectionClosedNormally); + Assert.IsNull(logger.RequestInfo); + Assert.IsTrue(logger.ConnectionClosedAbruptlyWithoutAnyRequestInfo); } - [Ignore] + [Test] public void CloseConnectionAbruptlyAfterSendingBeginRequestRecord() { + var logger = new OneRequestTestLogger(); + + using (var server = GetHelloWorldBoundServer()) + { + server.SetLogger(logger); + server.Start(true); + + // Just connect and quit + using (var sock = ConnectAndGetSocket()) + { + var beginReq = new BeginRequestRecord(1); + var req = new Request(sock, beginReq); + req.Send(beginReq); + sock.Close(); + } + + //TODO: Can't we do better than this? + System.Threading.Thread.Sleep(10); + } + + Assert.IsTrue(logger.ConnectionWasReceived); + Assert.IsFalse(logger.ConnectionClosedNormally); + Assert.IsNotNull(logger.RequestInfo); + Assert.IsFalse(logger.ConnectionClosedAbruptlyWithoutAnyRequestInfo); } [Ignore] diff --git a/Fos.Tests/Logger/NormalConnectionClosing.cs b/Fos.Tests/Logger/NormalConnectionClosing.cs index 64618f2..b3ec2ef 100644 --- a/Fos.Tests/Logger/NormalConnectionClosing.cs +++ b/Fos.Tests/Logger/NormalConnectionClosing.cs @@ -20,12 +20,12 @@ public void StartAndStop() server.SetLogger(logger); server.Start(true); - Assert.AreEqual(true, logger.ServerWasStarted); - Assert.AreEqual(false, logger.ServerWasStopped); + Assert.IsTrue(logger.ServerWasStarted); + Assert.IsFalse(logger.ServerWasStopped); } - Assert.AreEqual(true, logger.ServerWasStopped); - Assert.AreEqual(false, logger.ConnectionWasReceived); + Assert.IsTrue(logger.ServerWasStopped); + Assert.IsFalse(logger.ConnectionWasReceived); } [Test] @@ -63,10 +63,10 @@ public void CheckBasicData() browser.ExecuteRequest("http://localhost/", "GET"); } - Assert.AreEqual(true, logger.ConnectionWasReceived); - Assert.AreEqual(true, logger.ConnectionClosedNormally); + Assert.IsTrue(logger.ConnectionWasReceived); + Assert.IsTrue(logger.ConnectionClosedNormally); Assert.That(logger.RequestInfo != null && logger.RequestInfo.RelativePath == "/" && logger.RequestInfo.ResponseStatusCode== 200); - Assert.AreEqual(false, logger.ConnectionClosedAbruptlyWithoutAnyRequestInfo); + Assert.IsFalse(logger.ConnectionClosedAbruptlyWithoutAnyRequestInfo); } } } diff --git a/Fos.Tests/OwinContext.cs b/Fos.Tests/OwinContext.cs index ebe66df..4d15855 100644 --- a/Fos.Tests/OwinContext.cs +++ b/Fos.Tests/OwinContext.cs @@ -38,7 +38,7 @@ public void PartialContextNoMethod() { var ctx = new OwinContext("1.0", TokenSource.Token); - Assert.AreEqual(false, ctx.HttpMethodDefined); + Assert.IsFalse(ctx.HttpMethodDefined); } [Test] @@ -46,7 +46,7 @@ public void PartialContextNoUri() { var ctx = new OwinContext("1.0", TokenSource.Token); - Assert.AreEqual(false, ctx.RelativePathDefined); + Assert.IsFalse(ctx.RelativePathDefined); } [Test] @@ -54,7 +54,7 @@ public void PartialContextNoResponse() { var ctx = new OwinContext("1.0", TokenSource.Token); - Assert.AreEqual(false, ctx.SomeResponseExists); + Assert.IsFalse(ctx.SomeResponseExists); } } } diff --git a/Fos.Tests/ResponseAndRequestHeaders.cs b/Fos.Tests/ResponseAndRequestHeaders.cs new file mode 100644 index 0000000..81537b9 --- /dev/null +++ b/Fos.Tests/ResponseAndRequestHeaders.cs @@ -0,0 +1,37 @@ +using System; +using NUnit.Framework; +using Fos; + +namespace Fos.Tests +{ + [TestFixture] + public class ResponseAndRequestHeaders + { + [Test] + public void HeadersValuesAreArrayCopies() + { + var headerDict = new HeaderDictionary(); + headerDict.Add("Name", new string[1] { "Value" }); + + string[] copy = headerDict["Name"]; + + Assert.AreEqual("Value", copy [0]); + + // Change and make sure it is still the same in the original! + copy [0] = "Something different"; + + string[] copy2 = headerDict["Name"]; + Assert.AreEqual("Value", copy2 [0]); + } + + [Test] + public void OrdinalInsensitiveKeyComparison() + { + var headerDict = new HeaderDictionary(); + headerDict.Add("Name", new string[1] { "Value" }); + + Assert.Throws(() => headerDict.Add("nAmE", new string[1] { "whatever" })); + } + } +} + diff --git a/Fos/AssemblyInfo.cs b/Fos/AssemblyInfo.cs index 05c50f4..9c2fcc0 100644 --- a/Fos/AssemblyInfo.cs +++ b/Fos/AssemblyInfo.cs @@ -17,7 +17,7 @@ // The form "{Major}.{Minor}.*" will automatically update the build and revision, // and "{Major}.{Minor}.{Build}.*" will update just the revision. -[assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.1.0.0")] // The following attributes are used to specify the signing key for the assembly, // if desired. See the Mono documentation for more information about signing. diff --git a/Fos/Fos.csproj b/Fos/Fos.csproj index 5433d84..c35df40 100644 --- a/Fos/Fos.csproj +++ b/Fos/Fos.csproj @@ -57,6 +57,10 @@ + + + + diff --git a/Fos/FosSelfHost.cs b/Fos/FosSelfHost.cs index 674d98f..1462577 100644 --- a/Fos/FosSelfHost.cs +++ b/Fos/FosSelfHost.cs @@ -33,7 +33,7 @@ public bool IsRunning /// /// Starts this FastCgi server! This method only returns when the server is ready to accept connections. /// - /// True if this method starts the server without blocking, false to block. + /// True to start the server without blocking, false to block. public void Start(bool background) { AppBuilder = new FCgiAppBuilder(OnAppDisposal.Token); diff --git a/Fos/Listener/SocketListener.cs b/Fos/Listener/SocketListener.cs index a893ab0..1188b33 100644 --- a/Fos/Listener/SocketListener.cs +++ b/Fos/Listener/SocketListener.cs @@ -229,6 +229,18 @@ private void Work() } catch (Exception e) { + var sockEx = e as SocketException; + if (sockEx != null) + { + // Connection reset is a very rude way of the other side closing the connection, but it could happen. + if (sockEx.SocketErrorCode == SocketError.ConnectionReset) + continue; + + // Interrupted can also happen + else if (sockEx.SocketErrorCode == SocketError.Interrupted) + continue; + } + if (Logger != null) Logger.LogServerError(e, "Exception would end the data receiving loop. This is extremely bad. Please file a bug report."); } @@ -270,7 +282,8 @@ void BeginAcceptNewConnections(Socket listenSocket) } catch (Exception e) { - //TODO: Handle it + if (Logger != null) + Logger.LogSocketError(listenSocket, e, "Error when accepting connection on the listen socket."); } } diff --git a/Fos/Logging/ApplicationError.cs b/Fos/Logging/ApplicationError.cs new file mode 100644 index 0000000..2169910 --- /dev/null +++ b/Fos/Logging/ApplicationError.cs @@ -0,0 +1,34 @@ +using System; + +namespace Fos.Logging +{ + public class ApplicationError + { + public string HttpMethod { get; private set; } + public string RelativePath { get; private set; } + + /// + /// All request cookies concatenaded as one big string. + /// + public string Cookies { get; private set; } + + public Exception Error { get; private set; } + + /// + /// The time when the application error happened, in UTC. + /// + public DateTime When { get; private set; } + + internal ApplicationError(string verb, string relativePath, Exception e) + { + HttpMethod = verb; + RelativePath = relativePath; + Error = e; + + //TODO: Cookies + + When = DateTime.UtcNow; + } + } +} + diff --git a/Fos/Logging/StatsLogger.cs b/Fos/Logging/StatsLogger.cs index 6e1ec24..80a3f9b 100644 --- a/Fos/Logging/StatsLogger.cs +++ b/Fos/Logging/StatsLogger.cs @@ -36,13 +36,12 @@ internal RequestTimes(string verb, string relativePath, TimeSpan onlyTime) /// This class helps collect connection statistics. It could be entirely implemented outside Fos, but remains here /// for its usefulness. All DateTimes are record in UTC time. /// - /// THIS IS NOT IMPLEMENTED YET. DON'T USE IT! public class StatsLogger : IServerLogger { //TODO: Case sensitive or insensitive path comparison (and super option to supply IComparer) //TODO: Light weight locks? - private object connectionReceivedLock = new object(); + private readonly object connectionReceivedLock = new object(); public TimeSpan AggregationInterval { get; private set; } public ulong TotalConnectionsReceived { get; private set; } @@ -74,7 +73,6 @@ public class StatsLogger : IServerLogger /// /// Abruptly closed connections do not interfere with this data. private ConcurrentDictionary> TimesPerEndpoint = new ConcurrentDictionary>(); - //TODO: Use concurrent list of some sort /// /// Enumerates all requests time info with response status 200 and 301 ordered by average response time, descending. @@ -84,6 +82,20 @@ public IEnumerable GetAllRequestTimes() { return TimesPerEndpoint.Values.SelectMany(list => list).OrderByDescending(req => req.AverageTime); } + + private readonly object applicationErrorsLock = new object(); + /// + /// Application errors per request path. + /// + private LinkedList ApplicationErrors = new LinkedList(); + + /// + /// Enumerates all application errors in descending order of when they happened. + /// + public IEnumerable GetAllApplicationErrors() + { + return ApplicationErrors.Reverse(); + } public IList ServerStarted { get; private set; } public IList ServerStopped { get; private set; } @@ -120,6 +132,30 @@ private TimeSpan StopConnectionTimer(Socket s) return stopWatch.Elapsed; } + private readonly object lockPerPathObj = new object(); + /// + /// If a lock to access the LinkedList<RequestTime> for path already exists, it returns that lock. Otherwise, it creates one + /// and returns it. This method avoids using a global lock as much as possible. + /// + private object CreateAndReturnRelativePathLock(string relativePath, out LinkedList requestTimes) + { + if (!TimesPerEndpoint.TryGetValue(relativePath, out requestTimes)) + { + lock (lockPerPathObj) + { + // We need to check again + if (!TimesPerEndpoint.TryGetValue(relativePath, out requestTimes)) + { + requestTimes = new LinkedList(); + TimesPerEndpoint[relativePath] = requestTimes; + } + } + } + + // Return the list itself as the locking object + return requestTimes; + } + public void LogConnectionReceived(Socket createdSocket) { var now = Now; @@ -140,12 +176,12 @@ public void LogConnectionReceived(Socket createdSocket) LastAggregationPeriodStart += AggregationInterval; lastAggrPeriod = LastAggregationPeriodStart; } - - // Start the Watch - var stopWatch = new Stopwatch(); - RequestWatches[createdSocket] = stopWatch; - stopWatch.Start(); } + + // Start the Watch + var stopWatch = new Stopwatch(); + RequestWatches[createdSocket] = stopWatch; + stopWatch.Start(); // Add it to the aggregated connections ConnectionsReceivedAggregated[lastAggrPeriod] = ConnectionsReceivedAggregated[lastAggrPeriod] + 1; @@ -175,52 +211,54 @@ public void LogConnectionEndedNormally(Socket s, RequestInfo req) return; // Look for the times with our method - var timesForEndpoint = TimesPerEndpoint[req.RelativePath]; - if (TimesPerEndpoint == null) - { - timesForEndpoint = new LinkedList(); - TimesPerEndpoint[req.RelativePath] = timesForEndpoint; - } - - var verbTimes = timesForEndpoint.FirstOrDefault(t => t.HttpMethod == req.HttpMethod); - if (verbTimes == null) - { - // First request to this endpoint with this method. Add it. - verbTimes = new RequestTimes(req.HttpMethod, req.RelativePath, requestTime); - timesForEndpoint.AddLast(verbTimes); - } - else - { - // Just update the times - if (verbTimes.MinimumTime > requestTime) - verbTimes.MinimumTime = requestTime; - if (verbTimes.MaximumTime < requestTime) - verbTimes.MaximumTime = requestTime; + // We could do with a global lock here, but that just sounds so bad. + // Instead, we try to avoid a global lock as much as possible, and count on another method + // to give us a finer grained lock. + LinkedList timesForEndpoint; + lock (CreateAndReturnRelativePathLock(req.RelativePath, out timesForEndpoint)) + { + var verbTimes = timesForEndpoint.FirstOrDefault(t => t.HttpMethod == req.HttpMethod); + if (verbTimes == null) + { + // First request to this endpoint with this method. Add it. + verbTimes = new RequestTimes(req.HttpMethod, req.RelativePath, requestTime); + timesForEndpoint.AddLast(verbTimes); + } else + { + // Just update the times + if (verbTimes.MinimumTime > requestTime) + verbTimes.MinimumTime = requestTime; + if (verbTimes.MaximumTime < requestTime) + verbTimes.MaximumTime = requestTime; - //TODO: Precision issues with the line below - verbTimes.AverageTime = new TimeSpan((verbTimes.NumRequests * verbTimes.AverageTime.Ticks + requestTime.Ticks) / (verbTimes.NumRequests + 1)); - verbTimes.NumRequests++; - } + //TODO: Precision issues with the line below + verbTimes.AverageTime = new TimeSpan((verbTimes.NumRequests * verbTimes.AverageTime.Ticks + requestTime.Ticks) / (verbTimes.NumRequests + 1)); + verbTimes.NumRequests++; + } + } } public void LogApplicationError(Exception e, RequestInfo req) { - throw new NotImplementedException (); + lock (applicationErrorsLock) + { + ApplicationErrors.AddLast(new ApplicationError(req.HttpMethod, req.RelativePath, e)); + } } public void LogServerError(Exception e, string format, params object[] prms) { - throw new NotImplementedException (); + //throw new NotImplementedException (); } public void LogSocketError(Socket s, Exception e, string format, params object[] prms) { - throw new NotImplementedException (); + //throw new NotImplementedException (); } public void LogInvalidRecordReceived(RecordBase invalidRecord) { - throw new NotImplementedException (); + //throw new NotImplementedException (); } /// diff --git a/Fos/Logging/StatsPageMiddleware.cs b/Fos/Logging/StatsPageMiddleware.cs new file mode 100644 index 0000000..15bc15d --- /dev/null +++ b/Fos/Logging/StatsPageMiddleware.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.IO; + +namespace Fos.Logging +{ + using OwinHandler = Func, Task>; + + public class StatsPageMiddleware + { + private StatsLogger Logger; + + private string HtmlEncode(string text) + { + //TODO: THIS MUST BE VERY SECURE! IS IT SECURE ENOUGH? + if (text == null) + return string.Empty; + + return text.Replace("<", "<").Replace(">", ">"); + } + + private string Head() + { + return ""; + } + + private string EndOfBody() + { + return @""; + } + + private string Body() + { + var builder = new System.Text.StringBuilder(); + builder.AppendLine("

Overall

"); + builder.AppendFormat("Total connections received: {0}
", Logger.TotalConnectionsReceived); + + builder.AppendLine("

Request Times

"); + builder.AppendLine(""); + int i = 0; + foreach (var time in Logger.GetAllRequestTimes()) + { + string row_class = (i % 2 == 0) ? "even_row" : "odd_row"; + builder.AppendFormat("\n", row_class, HtmlEncode(time.RelativePath), HtmlEncode(time.HttpMethod), time.NumRequests); + + // Now the line with more data + builder.AppendFormat("\n", time.MinimumTime.TotalMilliseconds, time.MaximumTime.TotalMilliseconds, time.AverageTime.TotalMilliseconds); + + i++; + } + builder.Append("
PathMethodNumber of requests
{1}{2}{3}details
Minimum time: {0}ms
Maximum time: {1}ms
Average time: {2}ms
\n"); + + builder.Append("

Application Errors

"); + builder.AppendLine(""); + i = 0; + foreach (var error in Logger.GetAllApplicationErrors()) + { + string row_class = (i % 2 == 0) ? "even_row" : "odd_row"; + builder.AppendFormat("\n", row_class, HtmlEncode(error.RelativePath), HtmlEncode(error.HttpMethod), error.Error.Message); + + // Now the line with more data + builder.AppendFormat("\n", error.Error.ToString()); + + i++; + } + builder.Append("
PathMethodError
{1}{2}{3}details
{0}
\n"); + + return builder.ToString(); + } + + public Task Invoke(IDictionary owinPrms) + { + return Task.Factory.StartNew(() => { + var responseBody = (Stream) owinPrms["owin.ResponseBody"]; + + // Sets the headers + var headers = (IDictionary) owinPrms["owin.ResponseHeaders"]; + headers.Add("Content-Type", new[] { "text/html" }); + + // Sends the bodies + using (var writer = new StreamWriter(responseBody)) + { + writer.Write("Access Statistics"); + writer.Write(Head()); + writer.Write(""); + + writer.Write(Body()); + + writer.Write(EndOfBody()); + writer.Write(""); + } + }); + } + + public StatsPageMiddleware(OwinHandler next, StatsLogger logger) + { + if (next != null) + throw new ArgumentNullException("This middleware must be the last in the pipeline"); + else if (logger == null) + throw new ArgumentNullException("You must provide an IServerLogger"); + + Logger = logger; + } + } +} + diff --git a/Fos/Middleware/PageNotFoundMiddleware.cs b/Fos/Middleware/PageNotFoundMiddleware.cs index db65196..c26ed5e 100644 --- a/Fos/Middleware/PageNotFoundMiddleware.cs +++ b/Fos/Middleware/PageNotFoundMiddleware.cs @@ -25,16 +25,7 @@ private Task Invoke(IDictionary owinParameters) { var prms = (OwinContext) owinParameters; - int httpStatusCode; - try - { - httpStatusCode = (int)prms["owin.ResponseStatusCode"]; - } - catch (KeyNotFoundException) - { - // Default status code - httpStatusCode = 200; - } + int httpStatusCode = prms.ResponseStatusCode; if (httpStatusCode == 404) { diff --git a/Fos/Middleware/ShuntMiddleware.cs b/Fos/Middleware/ShuntMiddleware.cs new file mode 100644 index 0000000..2d30cbc --- /dev/null +++ b/Fos/Middleware/ShuntMiddleware.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Owin; + +namespace Fos +{ + using OwinHandler = Func, Task>; + + /// + /// Maps requests to certain paths (supplied as an IDictionary<string, IAppBuilder>) if the beginning of the request URL + /// matches the string supplied in a folder-like way (e.g. /some/request/to/page will match an entry "/some" from the dictionary). + /// Any request that does not match an entry is passed to the next handler in the pipeline. + /// + /// + /// Every string in the path mapping dictionary must start with a slash and must not end with a slash. + /// The context is never modified, whether a match is found or not. + /// + public class ShuntMiddleware + { + private OwinHandler Next; + private IDictionary Paths; + + private readonly object buildLock = new object(); + private IDictionary ShuntsHandlers = new Dictionary(); + + private IAppBuilder GetMatch(string requestPath) + { + string folderDir; + int secondSlash = requestPath.IndexOf('/', 1); + if (secondSlash > 0) + folderDir = requestPath.Substring(0, secondSlash); + else + folderDir = requestPath; + + IAppBuilder builder; + if (Paths.TryGetValue(folderDir, out builder)) + return builder; + else + return null; + } + + public Task Invoke(IDictionary context) + { + string requestPath = (string)context["owin.RequestPath"]; + var appBuilder = GetMatch(requestPath); + + if (appBuilder == null) + { + if (Next == null) + throw new EntryPointNotFoundException("No match found and no next handler defined"); //TODO: Is this exception appropriate? + + return Next(context); + } + + // Builds the pipeline to be invoked and cache it + OwinHandler handler; + lock (buildLock) + { + if (!ShuntsHandlers.TryGetValue(appBuilder, out handler)) + { + handler = (OwinHandler)appBuilder.Build(typeof(OwinHandler)); + ShuntsHandlers.Add(appBuilder, handler); + } + } + + return handler(context); + } + + public ShuntMiddleware(OwinHandler next, IDictionary buildersPaths) + { + Next = next; + Paths = buildersPaths ?? new Dictionary(); + } + } +} diff --git a/Fos/Owin/FCgiAppBuilder.cs b/Fos/Owin/FCgiAppBuilder.cs index 1385766..2c859cc 100644 --- a/Fos/Owin/FCgiAppBuilder.cs +++ b/Fos/Owin/FCgiAppBuilder.cs @@ -8,16 +8,16 @@ namespace Fos.Owin { public class FCgiAppBuilder : IAppBuilder { - Dictionary properties; + private Dictionary properties; public CancellationToken OnAppDisposing { get; private set; } - FCgiOwinRoot RootMiddleware; + private FCgiOwinRoot RootMiddleware; /// /// This is the last middleware added through , or the in case has not been called yet. /// - OwinMiddleware LastMiddleware; + private OwinMiddleware LastMiddleware; public FCgiAppBuilder(CancellationToken cancelToken) { @@ -29,7 +29,7 @@ public FCgiAppBuilder(CancellationToken cancelToken) properties.Add("host.OnAppDisposing", cancelToken); } - public IAppBuilder Use (object middleware, params object[] args) + public IAppBuilder Use(object middleware, params object[] args) { Delegate delegateMiddleware = middleware as Delegate; OwinMiddleware newMiddleware; diff --git a/Fos/Owin/HeaderDictionary.cs b/Fos/Owin/HeaderDictionary.cs new file mode 100644 index 0000000..8c8e0c5 --- /dev/null +++ b/Fos/Owin/HeaderDictionary.cs @@ -0,0 +1,156 @@ +using System; +using System.Linq; +using System.Collections.Generic; + +namespace Fos +{ + internal class HeaderDictionary : IDictionary + { + private Dictionary Headers; + + #region IDictionary implementation + + public void Add(string key, string[] value) + { + Headers.Add(key, value); + } + + public bool ContainsKey(string key) + { + return Headers.ContainsKey(key); + } + + public bool Remove(string key) + { + return Headers.Remove(key); + } + + private string[] CreateArrayCopy(string[] original) + { + string[] copy = new string[original.Length]; + Array.Copy(original, copy, original.Length); + return copy; + } + + public bool TryGetValue(string key, out string[] value) + { + string[] original; + if (Headers.TryGetValue(key, out original)) + { + // Return a copy of the array (owin spec) + value = CreateArrayCopy(original); + + return true; + } + else + { + value = null; + return false; + } + } + + public string[] this[string key] + { + get + { + // Return a copy of the array (owin spec) + return CreateArrayCopy(Headers[key]); + } + set + { + Headers[key] = value; + } + } + + public ICollection Keys + { + get + { + return Headers.Keys; + } + } + + public ICollection Values + { + get + { + var listOfValues = Headers.Values.ToList(); + for (int i = 0; i < listOfValues.Count; ++i) + { + // Make a copy of the array (owin spec) + string[] original = listOfValues[i]; + listOfValues[i] = new string[1] { original[0] }; + } + + return listOfValues; + } + } + + #endregion + + #region ICollection implementation + + public void Add(KeyValuePair item) + { + Headers.Add(item.Key, item.Value); + } + + public void Clear() + { + Headers.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((ICollection>)Headers).Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((ICollection>)Headers).CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + return ((ICollection>)Headers).Remove(item); + } + + public int Count + { + get + { + return Headers.Count; + } + } + + public bool IsReadOnly + { + get + { + return false; + } + } + + #endregion + + #region IEnumerable implementation + + public IEnumerator> GetEnumerator() + { + return ((IEnumerable>)Headers).GetEnumerator(); + } + + #endregion + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + public HeaderDictionary() + { + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + } +} + diff --git a/Fos/Owin/OwinContext.cs b/Fos/Owin/OwinContext.cs index 67ad6f7..bf16041 100644 --- a/Fos/Owin/OwinContext.cs +++ b/Fos/Owin/OwinContext.cs @@ -12,7 +12,7 @@ internal class OwinContext : IDictionary /// /// The parameters dictionary of the owin pipeline, built through this class's methods. /// - Dictionary parametersDictionary; + private Dictionary parametersDictionary; #region IDictionary implementation public void Add (string key, object value) @@ -110,8 +110,8 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () } #endregion - Dictionary requestHeaders; - Dictionary responseHeaders; + private HeaderDictionary requestHeaders; + private HeaderDictionary responseHeaders; public CancellationToken CancellationToken { get; private set; } @@ -378,8 +378,8 @@ public OwinContext(string owinVersion, CancellationToken token) throw new ArgumentException("Owin Version must be equal to '1.0'"); parametersDictionary = new Dictionary(); - requestHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); - responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + requestHeaders = new HeaderDictionary(); + responseHeaders = new HeaderDictionary(); Set("owin.RequestHeaders", requestHeaders); Set("owin.ResponseHeaders", responseHeaders);