diff --git a/CHANGELOG.md b/CHANGELOG.md index 680c2640..1515ba54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Current Version +v6.3.x + +- Minor change to chunked transfer, i.e. `SendChunk` now accepts `isFinal` as a `Boolean` property +- Added support for server-sent events, included `Test.ServerSentEvents` project +- Minor internal refactor + v6.2.x - Support for specifying exception handler for static, content, parameter, and dynamic routes (thank you @nomadeon) diff --git a/README.md b/README.md index 6d329e59..e716a0a6 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,11 @@ Special thanks to @DamienDennehy for allowing us the use of the ```Watson.Core`` This project is part of the [.NET Foundation](http://www.dotnetfoundation.org/projects) along with other projects like [the .NET Runtime](https://github.com/dotnet/runtime/). -## New in v6.2.x +## New in v6.3.x -- Support for specifying exception handler for static, content, parameter, and dynamic routes (thank you @nomadeon) +- Minor change to chunked transfer, i.e. `SendChunk` now accepts `isFinal` as a `Boolean` property +- Added support for server-sent events, included `Test.ServerSentEvents` project +- Minor internal refactor ## Special Thanks @@ -232,13 +234,16 @@ static async Task DownloadChunkedFile(HttpContextBase ctx) while (true) { int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length); + byte[] data = new byte[bytesRead]; + Buffer.BlockCopy(buffer, 0, bytesRead, data, 0); // only copy the read data + if (bytesRead > 0) { - await ctx.Response.SendChunk(buffer, bytesRead); + await ctx.Response.SendChunk(data, false); } else { - await ctx.Response.SendFinalChunk(null, 0); + await ctx.Response.SendChunk(Array.Empty(), true); break; } } @@ -248,6 +253,35 @@ static async Task DownloadChunkedFile(HttpContextBase ctx) } ``` +## Server-Sent Events + +Watson supports sending server-sent events. Refer to `Test.ServerSentEvents` for a sample implementation. The `SendEvent` method handles prepending `data: ` and the following `\n\n` to your message. + +### Sending Events + +```csharp +static async Task SendEvents(HttpContextBase ctx) +{ + ctx.Response.StatusCode = 200; + ctx.Response.ServerSentEvents = true; + + while (true) + { + string data = GetNextEvent(); // your implementation + if (!String.IsNullOrEmpty(data)) + { + await ctx.Response.SendEvent(data); + } + else + { + break; + } + } + + return; +} +``` + ## HostBuilder `HostBuilder` helps you set up your server much more easily by introducing a chain of settings and routes instead of using the server class directly. Special thanks to @sapurtcomputer30 for producing this fine feature! diff --git a/src/Test.Authentication/Program.cs b/src/Test.Authentication/Program.cs index 7f528b51..4c7d7167 100644 --- a/src/Test.Authentication/Program.cs +++ b/src/Test.Authentication/Program.cs @@ -1,16 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using RestWrapper; -using WatsonWebserver; -using WatsonWebserver.Core; -using WatsonWebserver.Lite; - -namespace Test.Authentication +namespace Test.Authentication { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using RestWrapper; + using WatsonWebserver; + using WatsonWebserver.Core; + using WatsonWebserver.Lite; + static class Program { #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously diff --git a/src/Test.ChunkServer/Program.cs b/src/Test.ChunkServer/Program.cs index 2256540d..d3d5510b 100644 --- a/src/Test.ChunkServer/Program.cs +++ b/src/Test.ChunkServer/Program.cs @@ -1,25 +1,27 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using WatsonWebserver; -using WatsonWebserver.Core; -using WatsonWebserver.Lite; - -namespace Test +namespace Test { - static class Program + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using WatsonWebserver; + using WatsonWebserver.Core; + using WatsonWebserver.Lite; + + public static class Program { +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + static bool _UsingLite = false; static string _Hostname = "localhost"; static int _Port = 8080; static WebserverSettings _Settings = null; static WebserverBase _Server = null; - static void Main(string[] args) + static async Task Main(string[] args) { if (args != null && args.Length > 0) { @@ -117,13 +119,13 @@ static async Task DefaultRoute(HttpContextBase ctx) if (bytesRead == buffer.Length) { - await ctx.Response.SendChunk(buffer); + await ctx.Response.SendChunk(buffer, false); } else { byte[] temp = new byte[bytesRead]; Buffer.BlockCopy(buffer, 0, temp, 0, bytesRead); - await ctx.Response.SendChunk(temp); + await ctx.Response.SendChunk(temp, false); } } else @@ -132,13 +134,13 @@ static async Task DefaultRoute(HttpContextBase ctx) if (bytesRead == buffer.Length) { - await ctx.Response.SendFinalChunk(buffer); + await ctx.Response.SendChunk(buffer, true); } else { byte[] temp = new byte[bytesRead]; Buffer.BlockCopy(buffer, 0, temp, 0, bytesRead); - await ctx.Response.SendFinalChunk(temp); + await ctx.Response.SendChunk(temp, true); } } @@ -183,13 +185,13 @@ static async Task DefaultRoute(HttpContextBase ctx) if (bytesRead == buffer.Length) { - await ctx.Response.SendChunk(buffer); + await ctx.Response.SendChunk(buffer, false); } else { byte[] temp = new byte[bytesRead]; Buffer.BlockCopy(buffer, 0, temp, 0, bytesRead); - await ctx.Response.SendChunk(temp); + await ctx.Response.SendChunk(temp, false); } } else @@ -198,16 +200,14 @@ static async Task DefaultRoute(HttpContextBase ctx) if (bytesRead == buffer.Length) { - await ctx.Response.SendFinalChunk(buffer); + await ctx.Response.SendChunk(buffer, true); } else { byte[] temp = new byte[bytesRead]; Buffer.BlockCopy(buffer, 0, temp, 0, bytesRead); - await ctx.Response.SendFinalChunk(temp); + await ctx.Response.SendChunk(temp, true); } - - await ctx.Response.SendFinalChunk(buffer); } bytesSent += bytesRead; @@ -230,6 +230,8 @@ static async Task DefaultRoute(HttpContextBase ctx) { Console.WriteLine(e.ToString()); } - } + } + +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously } } diff --git a/src/Test.ChunkServer/Test.ChunkServer.csproj b/src/Test.ChunkServer/Test.ChunkServer.csproj index d1785be2..accccb71 100644 --- a/src/Test.ChunkServer/Test.ChunkServer.csproj +++ b/src/Test.ChunkServer/Test.ChunkServer.csproj @@ -2,7 +2,7 @@ Exe - net462;net48;net6.0;net7.0;net8.0 + net462;net48;net6.0;net8.0 @@ -16,6 +16,7 @@ + diff --git a/src/Test.DataReader/Program.cs b/src/Test.DataReader/Program.cs index 1e7842d7..9b032446 100644 --- a/src/Test.DataReader/Program.cs +++ b/src/Test.DataReader/Program.cs @@ -1,15 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using GetSomeInput; -using WatsonWebserver; -using WatsonWebserver.Core; -using WatsonWebserver.Lite; - -namespace Test +namespace Test { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using GetSomeInput; + using WatsonWebserver; + using WatsonWebserver.Core; + using WatsonWebserver.Lite; + static class Program { static bool _UsingLite = false; diff --git a/src/Test.DataReader/Test.DataReader.csproj b/src/Test.DataReader/Test.DataReader.csproj index 5299ad85..990c8313 100644 --- a/src/Test.DataReader/Test.DataReader.csproj +++ b/src/Test.DataReader/Test.DataReader.csproj @@ -2,7 +2,7 @@ Exe - net462;net48;net6.0;net7.0;net8.0 + net462;net48;net6.0;net8.0 diff --git a/src/Test.Default/Program.cs b/src/Test.Default/Program.cs index b55d5e6f..40d62fa7 100644 --- a/src/Test.Default/Program.cs +++ b/src/Test.Default/Program.cs @@ -1,5 +1,4 @@ -using GetSomeInput; -namespace Test +namespace Test { using System; using System.Collections.Generic; @@ -7,6 +6,7 @@ namespace Test using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; + using GetSomeInput; using WatsonWebserver; using WatsonWebserver.Core; using WatsonWebserver.Lite; diff --git a/src/Test.Default/Test.Default.csproj b/src/Test.Default/Test.Default.csproj index 3755ac23..969cb950 100644 --- a/src/Test.Default/Test.Default.csproj +++ b/src/Test.Default/Test.Default.csproj @@ -2,7 +2,7 @@ Exe - net462;net48;net6.0;net7.0;net8.0 + net462;net48;net6.0;net8.0 diff --git a/src/Test.Docker/Test.Docker.csproj b/src/Test.Docker/Test.Docker.csproj index 45b405d8..27f13694 100644 --- a/src/Test.Docker/Test.Docker.csproj +++ b/src/Test.Docker/Test.Docker.csproj @@ -2,7 +2,7 @@ Exe - net462;net48;net6.0;net7.0;net8.0 + net462;net48;net6.0;net8.0 diff --git a/src/Test.HostBuilder/Program.cs b/src/Test.HostBuilder/Program.cs index dae4159a..743b4850 100644 --- a/src/Test.HostBuilder/Program.cs +++ b/src/Test.HostBuilder/Program.cs @@ -1,14 +1,14 @@ -using RestWrapper; -using System; -using System.Collections.Generic; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using WatsonWebserver; -using WatsonWebserver.Core; -using WatsonWebserver.Lite; - -namespace Test.HostBuilder +namespace Test.HostBuilder { + using System; + using System.Collections.Generic; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using RestWrapper; + using WatsonWebserver; + using WatsonWebserver.Core; + using WatsonWebserver.Lite; + public static class Program { #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously @@ -20,7 +20,7 @@ public static class Program static WebserverSettings _Settings = null; static WebserverBase _Server = null; - public static void Main(string[] args) + public static async Task Main(string[] args) { if (args != null && args.Length > 0) { @@ -156,7 +156,7 @@ public static void Main(string[] args) using (RestRequest req = new RestRequest(url)) { - using (RestResponse resp = req.Send()) + using (RestResponse resp = await req.SendAsync()) { Console.WriteLine("Received response: " + resp.StatusCode); Task.Delay(1000).Wait(); diff --git a/src/Test.HostBuilder/Test.HostBuilder.csproj b/src/Test.HostBuilder/Test.HostBuilder.csproj index 6ceb3415..7fbaf221 100644 --- a/src/Test.HostBuilder/Test.HostBuilder.csproj +++ b/src/Test.HostBuilder/Test.HostBuilder.csproj @@ -2,14 +2,15 @@ Exe - net462;net48;net6.0;net7.0;net8.0 + net462;net48;net6.0;net8.0 - + + diff --git a/src/Test.Loopback/Program.cs b/src/Test.Loopback/Program.cs index 649590d5..d75337d2 100644 --- a/src/Test.Loopback/Program.cs +++ b/src/Test.Loopback/Program.cs @@ -1,14 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using WatsonWebserver; -using WatsonWebserver.Core; -using WatsonWebserver.Lite; - -namespace Test.Loopback +namespace Test.Loopback { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using WatsonWebserver; + using WatsonWebserver.Core; + using WatsonWebserver.Lite; + static class Program { static bool _UsingLite = false; diff --git a/src/Test.Loopback/Test.Loopback.csproj b/src/Test.Loopback/Test.Loopback.csproj index 05a0d1a8..7bfc3049 100644 --- a/src/Test.Loopback/Test.Loopback.csproj +++ b/src/Test.Loopback/Test.Loopback.csproj @@ -2,7 +2,7 @@ Exe - net462;net48;net6.0;net7.0;net8.0 + net462;net48;net6.0;net8.0 diff --git a/src/Test.MaxConnections/Program.cs b/src/Test.MaxConnections/Program.cs index a2664cee..91177eb9 100644 --- a/src/Test.MaxConnections/Program.cs +++ b/src/Test.MaxConnections/Program.cs @@ -1,11 +1,11 @@ -using System; -using System.Threading.Tasks; -using WatsonWebserver; -using WatsonWebserver.Core; -using RestWrapper; - -namespace Test.MaxConnections +namespace Test.MaxConnections { + using System; + using System.Threading.Tasks; + using WatsonWebserver; + using WatsonWebserver.Core; + using RestWrapper; + class Program { static bool _UsingLite = false; @@ -69,12 +69,16 @@ static async Task DefaultRoute(HttpContextBase ctx) return; } - static void ClientTask() + static async Task ClientTask() { Console.WriteLine("Sending request"); - RestRequest req = new RestRequest("http://" + _Hostname + ":" + _Port + "/"); - RestResponse resp = req.Send(); - Console.WriteLine("Response received: " + resp.StatusDescription); + using (RestRequest req = new RestRequest("http://" + _Hostname + ":" + _Port + "/")) + { + using (RestResponse resp = await req.SendAsync()) + { + Console.WriteLine("Response received: " + resp.StatusDescription); + } + } } } } diff --git a/src/Test.MaxConnections/Test.MaxConnections.csproj b/src/Test.MaxConnections/Test.MaxConnections.csproj index 9eab1bb9..324e6cf7 100644 --- a/src/Test.MaxConnections/Test.MaxConnections.csproj +++ b/src/Test.MaxConnections/Test.MaxConnections.csproj @@ -2,11 +2,11 @@ Exe - net462;net48;net6.0;net7.0;net8.0 + net462;net48;net6.0;net8.0 - + diff --git a/src/Test.Routing/Program.cs b/src/Test.Routing/Program.cs index 64a2e4c0..b82760c5 100644 --- a/src/Test.Routing/Program.cs +++ b/src/Test.Routing/Program.cs @@ -1,16 +1,16 @@ -using RestWrapper; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using WatsonWebserver; -using WatsonWebserver.Core; -using WatsonWebserver.Lite; - -namespace Test +namespace Test { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using RestWrapper; + using WatsonWebserver; + using WatsonWebserver.Core; + using WatsonWebserver.Lite; + static class Program { #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously diff --git a/src/Test.Routing/Test.Routing.csproj b/src/Test.Routing/Test.Routing.csproj index 7aa3e07e..ce5ea6f9 100644 --- a/src/Test.Routing/Test.Routing.csproj +++ b/src/Test.Routing/Test.Routing.csproj @@ -2,7 +2,7 @@ Exe - net6.0;net7.0;net8.0 + net6.0;net8.0 diff --git a/src/Test.Serialization/Program.cs b/src/Test.Serialization/Program.cs index 203051de..0bbbf168 100644 --- a/src/Test.Serialization/Program.cs +++ b/src/Test.Serialization/Program.cs @@ -1,15 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using GetSomeInput; -using WatsonWebserver; -using WatsonWebserver.Core; -using WatsonWebserver.Lite; - -namespace Test.Serialization +namespace Test.Serialization { + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using GetSomeInput; + using Newtonsoft.Json; + using Newtonsoft.Json.Serialization; + using WatsonWebserver; + using WatsonWebserver.Core; + using WatsonWebserver.Lite; + static class Program { static bool _UsingLite = false; diff --git a/src/Test.Serialization/Test.Serialization.csproj b/src/Test.Serialization/Test.Serialization.csproj index 94e49fe4..f70cd8cb 100644 --- a/src/Test.Serialization/Test.Serialization.csproj +++ b/src/Test.Serialization/Test.Serialization.csproj @@ -2,7 +2,7 @@ Exe - net462;net48;net6.0;net7.0;net8.0 + net462;net48;net6.0;net8.0 diff --git a/src/Test.ServerSentEvents/Program.cs b/src/Test.ServerSentEvents/Program.cs new file mode 100644 index 00000000..775cab30 --- /dev/null +++ b/src/Test.ServerSentEvents/Program.cs @@ -0,0 +1,136 @@ +namespace Test.ServerSentEvents +{ + using System; + using System.Text; + using WatsonWebserver; + using WatsonWebserver.Core; + using WatsonWebserver.Lite; + + public static class Program + { +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + + static bool _UsingLite = false; + static string _Hostname = "localhost"; + static int _Port = 8080; + static WebserverSettings _Settings = null; + static WebserverBase _Server = null; + + static async Task Main(string[] args) + { + if (args != null && args.Length > 0) + { + if (args[0].Equals("lite")) _UsingLite = true; + } + + _Settings = new WebserverSettings + { + Hostname = _Hostname, + Port = _Port + }; + + if (_UsingLite) + { + Console.WriteLine("Initializing webserver lite"); + _Server = new WatsonWebserver.Lite.WebserverLite(_Settings, DefaultRoute); + } + else + { + Console.WriteLine("Initializing webserver"); + _Server = new Webserver(_Settings, DefaultRoute); + } + + Console.WriteLine("Listening on " + _Settings.Prefix); + Console.WriteLine("Use /txt/test.txt"); + _Server.Start(); + + Console.WriteLine("Press ENTER to exit"); + Console.ReadLine(); + } + + static async Task DefaultRoute(HttpContextBase ctx) + { + try + { + if (ctx.Request.Url.RawWithoutQuery.Equals("/txt/test.txt")) + { + Console.WriteLine("- User requested /txt/test.txt"); + ctx.Response.StatusCode = 200; + ctx.Response.ServerSentEvents = true; + + long fileSize = new FileInfo("./txt/test.txt").Length; + Console.WriteLine("Sending file of size " + fileSize + " bytes"); + + long bytesSent = 0; + + using (FileStream fs = new FileStream("./txt/test.txt", FileMode.Open, FileAccess.Read)) + { + byte[] buffer = new byte[16]; + long bytesRemaining = fileSize; + + while (bytesRemaining > 0) + { + Thread.Sleep(500); + int bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length); + + if (bytesRead > 0) + { + bytesRemaining -= bytesRead; + + if (bytesRemaining > 0) + { + Console.WriteLine("- Sending event of size " + bytesRead); + + if (bytesRead == buffer.Length) + { + await ctx.Response.SendEvent(Encoding.UTF8.GetString(buffer), false); + } + else + { + byte[] temp = new byte[bytesRead]; + Buffer.BlockCopy(buffer, 0, temp, 0, bytesRead); + await ctx.Response.SendEvent(Encoding.UTF8.GetString(temp), false); + } + } + else + { + Console.WriteLine("- Sending final chunk of size " + bytesRead); + + if (bytesRead == buffer.Length) + { + await ctx.Response.SendEvent(Encoding.UTF8.GetString(buffer), true); + } + else + { + byte[] temp = new byte[bytesRead]; + Buffer.BlockCopy(buffer, 0, temp, 0, bytesRead); + await ctx.Response.SendEvent(Encoding.UTF8.GetString(temp), true); + } + } + + bytesSent += bytesRead; + } + } + } + + Console.WriteLine("Sent " + bytesSent + " bytes"); + return; + } + else + { + ctx.Response.StatusCode = 200; + await ctx.Response.Send("Watson says try using GET /txt/test.txt to see what happens!"); + return; + } + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + } + +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } +} \ No newline at end of file diff --git a/src/Test.ServerSentEvents/Test.ServerSentEvents.csproj b/src/Test.ServerSentEvents/Test.ServerSentEvents.csproj new file mode 100644 index 00000000..941b0014 --- /dev/null +++ b/src/Test.ServerSentEvents/Test.ServerSentEvents.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + Always + + + + diff --git a/src/Test.ServerSentEvents/txt/test.txt b/src/Test.ServerSentEvents/txt/test.txt new file mode 100644 index 00000000..00929d98 --- /dev/null +++ b/src/Test.ServerSentEvents/txt/test.txt @@ -0,0 +1,7 @@ +This is a random test file. + +I'm going to send this in 16-byte chunks. + +Hopefully this works exactly the way we expect it to. + +Wish me luck... diff --git a/src/Test.Stream/Program.cs b/src/Test.Stream/Program.cs index 7768353a..d3b48b76 100644 --- a/src/Test.Stream/Program.cs +++ b/src/Test.Stream/Program.cs @@ -1,13 +1,13 @@ -using System; -using System.IO; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using WatsonWebserver; -using WatsonWebserver.Core; - -namespace Test +namespace Test { + using System; + using System.IO; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using WatsonWebserver; + using WatsonWebserver.Core; + class Program { static bool _UsingLite = false; diff --git a/src/Test.Stream/Test.Stream.csproj b/src/Test.Stream/Test.Stream.csproj index 1649759e..323e4460 100644 --- a/src/Test.Stream/Test.Stream.csproj +++ b/src/Test.Stream/Test.Stream.csproj @@ -2,7 +2,7 @@ Exe - net462;net48;net6.0;net7.0;net8.0 + net462;net48;net6.0;net8.0 diff --git a/src/WatsonWebserver.Core/AccessControlManager.cs b/src/WatsonWebserver.Core/AccessControlManager.cs index 2e2ef89e..3e8a5dae 100644 --- a/src/WatsonWebserver.Core/AccessControlManager.cs +++ b/src/WatsonWebserver.Core/AccessControlManager.cs @@ -1,13 +1,13 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using IpMatcher; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using IpMatcher; + /// /// Access control manager. Dictates which connections are permitted or denied. /// diff --git a/src/WatsonWebserver.Core/AccessControlMode.cs b/src/WatsonWebserver.Core/AccessControlMode.cs index 249ec71c..35cf8765 100644 --- a/src/WatsonWebserver.Core/AccessControlMode.cs +++ b/src/WatsonWebserver.Core/AccessControlMode.cs @@ -1,12 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Runtime.Serialization; + using System.Text; + using System.Threading.Tasks; + /// /// Access control mode of operation. /// diff --git a/src/WatsonWebserver.Core/Chunk.cs b/src/WatsonWebserver.Core/Chunk.cs index 385f0ad0..263595d7 100644 --- a/src/WatsonWebserver.Core/Chunk.cs +++ b/src/WatsonWebserver.Core/Chunk.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Text; + /// /// A chunk of data, used when reading from a request where the Transfer-Encoding header includes 'chunked'. /// diff --git a/src/WatsonWebserver.Core/ConnectionReceivedEventArgs.cs b/src/WatsonWebserver.Core/ConnectionReceivedEventArgs.cs index bd443ade..e9f6984c 100644 --- a/src/WatsonWebserver.Core/ConnectionReceivedEventArgs.cs +++ b/src/WatsonWebserver.Core/ConnectionReceivedEventArgs.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Text; + /// /// Connection event arguments. /// diff --git a/src/WatsonWebserver.Core/ContentRoute.cs b/src/WatsonWebserver.Core/ContentRoute.cs index 23ebbb0c..73f9eab0 100644 --- a/src/WatsonWebserver.Core/ContentRoute.cs +++ b/src/WatsonWebserver.Core/ContentRoute.cs @@ -1,14 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using System.Text.Json; + using System.Text.Json.Serialization; + /// /// Assign a method handler for when requests are received matching the supplied method and path. /// diff --git a/src/WatsonWebserver.Core/ContentRouteManager.cs b/src/WatsonWebserver.Core/ContentRouteManager.cs index 991708d7..4c5dcb4d 100644 --- a/src/WatsonWebserver.Core/ContentRouteManager.cs +++ b/src/WatsonWebserver.Core/ContentRouteManager.cs @@ -1,12 +1,12 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + /// /// Content route manager. Content routes are used for GET and HEAD requests to specific files or entire directories. /// diff --git a/src/WatsonWebserver.Core/DefaultSerializationHelper.cs b/src/WatsonWebserver.Core/DefaultSerializationHelper.cs index 71121e34..72b2ed60 100644 --- a/src/WatsonWebserver.Core/DefaultSerializationHelper.cs +++ b/src/WatsonWebserver.Core/DefaultSerializationHelper.cs @@ -1,14 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Linq; -using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Globalization; + using System.Linq; + using System.Net; + using System.Text.Json; + using System.Text.Json.Serialization; + /// /// Default serialization helper. /// diff --git a/src/WatsonWebserver.Core/DestinationDetails.cs b/src/WatsonWebserver.Core/DestinationDetails.cs new file mode 100644 index 00000000..40c9df2f --- /dev/null +++ b/src/WatsonWebserver.Core/DestinationDetails.cs @@ -0,0 +1,128 @@ +namespace WatsonWebserver.Core +{ + using System; + using System.Collections; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Text; + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Threading; + using System.Threading.Tasks; + using Timestamps; + + /// + /// Destination details. + /// + public class DestinationDetails + { + #region Public-Members + + /// + /// IP address to which the request was made. + /// + public string IpAddress { get; set; } = null; + + /// + /// TCP port on which the request was received. + /// + public int Port + { + get + { + return _Port; + } + set + { + if (value < 0 || value > 65535) throw new ArgumentOutOfRangeException(nameof(Port)); + _Port = value; + } + } + + /// + /// Hostname to which the request was directed. + /// + public string Hostname { get; set; } = null; + + /// + /// Hostname elements. + /// + public string[] HostnameElements + { + get + { + string hostname = Hostname; + string[] ret; + + if (!String.IsNullOrEmpty(hostname)) + { + if (!IPAddress.TryParse(hostname, out _)) + { + ret = hostname.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries); + return ret; + } + else + { + ret = new string[1]; + ret[0] = hostname; + return ret; + } + } + + ret = new string[0]; + return ret; + } + } + + #endregion + + #region Private-Members + + private int _Port = 0; + + #endregion + + #region Constructors-and-Factories + + /// + /// Destination details. + /// + public DestinationDetails() + { + + } + + /// + /// Source details. + /// + /// IP address to which the request was made. + /// TCP port on which the request was received. + /// Hostname. + public DestinationDetails(string ip, int port, string hostname) + { + if (String.IsNullOrEmpty(ip)) throw new ArgumentNullException(nameof(ip)); + if (port < 0) throw new ArgumentOutOfRangeException(nameof(port)); + if (String.IsNullOrEmpty(hostname)) throw new ArgumentNullException(nameof(hostname)); + + IpAddress = ip; + Port = port; + Hostname = hostname; + } + + #endregion + + #region Public-Methods + + #endregion + + #region Private-Methods + + #endregion + } +} diff --git a/src/WatsonWebserver.Core/DynamicRoute.cs b/src/WatsonWebserver.Core/DynamicRoute.cs index 3bc8fb0c..55c74959 100644 --- a/src/WatsonWebserver.Core/DynamicRoute.cs +++ b/src/WatsonWebserver.Core/DynamicRoute.cs @@ -1,14 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using System.Text.Json; + using System.Text.Json.Serialization; + /// /// Assign a method handler for when requests are received matching the supplied method and path regex. /// diff --git a/src/WatsonWebserver.Core/DynamicRouteManager.cs b/src/WatsonWebserver.Core/DynamicRouteManager.cs index 4501f9ec..287951b6 100644 --- a/src/WatsonWebserver.Core/DynamicRouteManager.cs +++ b/src/WatsonWebserver.Core/DynamicRouteManager.cs @@ -1,13 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using RegexMatcher; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using RegexMatcher; + /// /// Dynamic route manager. Dynamic routes are used for requests using any HTTP method to any path that can be matched by regular expression. /// diff --git a/src/WatsonWebserver.Core/ExceptionEventArgs.cs b/src/WatsonWebserver.Core/ExceptionEventArgs.cs index 1112f813..cab4adb6 100644 --- a/src/WatsonWebserver.Core/ExceptionEventArgs.cs +++ b/src/WatsonWebserver.Core/ExceptionEventArgs.cs @@ -1,10 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Text; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Text; + /// /// Exception event arguments. /// diff --git a/src/WatsonWebserver.Core/HttpContextBase.cs b/src/WatsonWebserver.Core/HttpContextBase.cs index 1eb88339..1d344c25 100644 --- a/src/WatsonWebserver.Core/HttpContextBase.cs +++ b/src/WatsonWebserver.Core/HttpContextBase.cs @@ -1,13 +1,13 @@ -using System; -using System.IO; -using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using Timestamps; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.IO; + using System.Net; + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Threading; + using Timestamps; + /// /// HTTP context including both request and response. /// diff --git a/src/WatsonWebserver.Core/HttpMethod.cs b/src/WatsonWebserver.Core/HttpMethod.cs index df25351d..5cd2ce19 100644 --- a/src/WatsonWebserver.Core/HttpMethod.cs +++ b/src/WatsonWebserver.Core/HttpMethod.cs @@ -1,12 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Runtime.Serialization; + using System.Text; + using System.Threading.Tasks; + /// /// HTTP methods, i.e. GET, PUT, POST, DELETE, etc. /// diff --git a/src/WatsonWebserver.Core/HttpRequestBase.cs b/src/WatsonWebserver.Core/HttpRequestBase.cs index a358f63e..d3b012e1 100644 --- a/src/WatsonWebserver.Core/HttpRequestBase.cs +++ b/src/WatsonWebserver.Core/HttpRequestBase.cs @@ -1,22 +1,22 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using Timestamps; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Text; + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Threading; + using System.Threading.Tasks; + using Timestamps; + /// /// HTTP request. /// @@ -210,307 +210,5 @@ public NameValueCollection Headers #region Private-Methods #endregion - - #region Embedded-Classes - - /// - /// Source details. - /// - public class SourceDetails - { - /// - /// IP address of the requestor. - /// - public string IpAddress { get; set; } = null; - - /// - /// TCP port from which the request originated on the requestor. - /// - public int Port { get; set; } = 0; - - /// - /// Source details. - /// - public SourceDetails() - { - - } - - /// - /// Source details. - /// - /// IP address of the requestor. - /// TCP port from which the request originated on the requestor. - public SourceDetails(string ip, int port) - { - if (String.IsNullOrEmpty(ip)) throw new ArgumentNullException(nameof(ip)); - if (port < 0) throw new ArgumentOutOfRangeException(nameof(port)); - - IpAddress = ip; - Port = port; - } - } - - /// - /// Destination details. - /// - public class DestinationDetails - { - /// - /// IP address to which the request was made. - /// - public string IpAddress { get; set; } = null; - - /// - /// TCP port on which the request was received. - /// - public int Port { get; set; } = 0; - - /// - /// Hostname to which the request was directed. - /// - public string Hostname { get; set; } = null; - - /// - /// Hostname elements. - /// - public string[] HostnameElements - { - get - { - string hostname = Hostname; - string[] ret; - - if (!String.IsNullOrEmpty(hostname)) - { - if (!IPAddress.TryParse(hostname, out _)) - { - ret = hostname.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - return ret; - } - else - { - ret = new string[1]; - ret[0] = hostname; - return ret; - } - } - - ret = new string[0]; - return ret; - } - } - - /// - /// Destination details. - /// - public DestinationDetails() - { - - } - - /// - /// Source details. - /// - /// IP address to which the request was made. - /// TCP port on which the request was received. - /// Hostname. - public DestinationDetails(string ip, int port, string hostname) - { - if (String.IsNullOrEmpty(ip)) throw new ArgumentNullException(nameof(ip)); - if (port < 0) throw new ArgumentOutOfRangeException(nameof(port)); - if (String.IsNullOrEmpty(hostname)) throw new ArgumentNullException(nameof(hostname)); - - IpAddress = ip; - Port = port; - Hostname = hostname; - } - } - - /// - /// URL details. - /// - public class UrlDetails - { - /// - /// Full URL. - /// - public string Full { get; set; } = null; - - /// - /// Raw URL with query. - /// - public string RawWithQuery { get; set; } = null; - - /// - /// Raw URL without query. - /// - public string RawWithoutQuery - { - get - { - if (!String.IsNullOrEmpty(RawWithQuery)) - { - if (RawWithQuery.Contains("?")) return RawWithQuery.Substring(0, RawWithQuery.IndexOf("?")); - else return RawWithQuery; - } - else - { - return null; - } - } - } - - /// - /// Raw URL elements. - /// - public string[] Elements - { - get - { - string rawUrl = RawWithoutQuery; - - if (!String.IsNullOrEmpty(rawUrl)) - { - while (rawUrl.Contains("//")) rawUrl = rawUrl.Replace("//", "/"); - while (rawUrl.StartsWith("/")) rawUrl = rawUrl.Substring(1); - while (rawUrl.EndsWith("/")) rawUrl = rawUrl.Substring(0, rawUrl.Length - 1); - string[] encoded = rawUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); - if (encoded != null && encoded.Length > 0) - { - string[] decoded = new string[encoded.Length]; - for (int i = 0; i < encoded.Length; i++) - { - decoded[i] = WebUtility.UrlDecode(encoded[i]); - } - - return decoded; - } - } - - string[] ret = new string[0]; - return ret; - } - } - - /// - /// Parameters found within the URL, if using parameter routes. - /// - public NameValueCollection Parameters - { - get - { - return _Parameters; - } - set - { - if (value == null) _Parameters = new NameValueCollection(StringComparer.InvariantCultureIgnoreCase); - else _Parameters = value; - } - } - - /// - /// URL details. - /// - public UrlDetails() - { - - } - - /// - /// URL details. - /// - /// Full URL. - /// Raw URL. - public UrlDetails(string fullUrl, string rawUrl) - { - if (String.IsNullOrEmpty(rawUrl)) throw new ArgumentNullException(nameof(rawUrl)); - - Full = fullUrl; - RawWithQuery = rawUrl; - } - - private NameValueCollection _Parameters = new NameValueCollection(StringComparer.InvariantCultureIgnoreCase); - } - - /// - /// Query details. - /// - public class QueryDetails - { - /// - /// Querystring, excluding the leading '?'. - /// - public string Querystring - { - get - { - if (_FullUrl.Contains("?")) - { - return _FullUrl.Substring(_FullUrl.IndexOf("?") + 1, (_FullUrl.Length - _FullUrl.IndexOf("?") - 1)); - } - else - { - return null; - } - } - } - - /// - /// Query elements. - /// - public NameValueCollection Elements - { - get - { - NameValueCollection ret = new NameValueCollection(StringComparer.InvariantCultureIgnoreCase); - string qs = Querystring; - if (!String.IsNullOrEmpty(qs)) - { - string[] queries = qs.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries); - if (queries.Length > 0) - { - for (int i = 0; i < queries.Length; i++) - { - string[] queryParts = queries[i].Split('='); - if (queryParts != null && queryParts.Length == 2) - { - ret.Add(queryParts[0], queryParts[1]); - } - else if (queryParts != null && queryParts.Length == 1) - { - ret.Add(queryParts[0], null); - } - } - } - } - - return ret; - } - } - - /// - /// Query details. - /// - public QueryDetails() - { - - } - - /// - /// Query details. - /// - /// Full URL. - public QueryDetails(string fullUrl) - { - if (String.IsNullOrEmpty(fullUrl)) throw new ArgumentNullException(nameof(fullUrl)); - - _FullUrl = fullUrl; - } - - private string _FullUrl = null; - } - - #endregion } } diff --git a/src/WatsonWebserver.Core/HttpResponseBase.cs b/src/WatsonWebserver.Core/HttpResponseBase.cs index b23501e1..4a8a4ef0 100644 --- a/src/WatsonWebserver.Core/HttpResponseBase.cs +++ b/src/WatsonWebserver.Core/HttpResponseBase.cs @@ -1,20 +1,20 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Text.Json; -using System.Text.Json.Serialization; -using Timestamps; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using System.Text.Json; + using System.Text.Json.Serialization; + using Timestamps; + /// /// HTTP response. /// @@ -84,6 +84,11 @@ public NameValueCollection Headers /// public bool ChunkedTransfer { get; set; } = false; + /// + /// Indicates whether or not server-sent events should be indicated in the response. + /// + public bool ServerSentEvents { get; set; } = false; + /// /// Retrieve the response body sent using a Send() or SendAsync() method. /// @@ -162,20 +167,25 @@ public NameValueCollection Headers public abstract Task Send(long contentLength, Stream stream, CancellationToken token = default); /// - /// Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact. + /// Send headers (if not already sent) and a chunk of data using chunked transfer-encoding. + /// The connection will be kept in-tact unless isFinal is set to true. /// /// Chunk of data. + /// Boolean indicating if this is the final chunk. /// Cancellation token useful for canceling the request. /// True if successful. - public abstract Task SendChunk(byte[] chunk, CancellationToken token = default); + public abstract Task SendChunk(byte[] chunk, bool isFinal, CancellationToken token = default); /// - /// Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection. + /// Send headers (if not already sent) and a server-sent event. + /// Watson will handle prepending 'data: ' to your event data. + /// The connection will be kept in-tact unless isFinal is set to true. /// - /// Chunk of data. + /// Event data. + /// Boolean indicating if this is the final chunk. /// Cancellation token useful for canceling the request. /// True if successful. - public abstract Task SendFinalChunk(byte[] chunk, CancellationToken token = default); + public abstract Task SendEvent(string eventData, bool isFinal, CancellationToken token = default); #endregion diff --git a/src/WatsonWebserver.Core/IHostBuilder.cs b/src/WatsonWebserver.Core/IHostBuilder.cs index 41296c15..5aaac1a5 100644 --- a/src/WatsonWebserver.Core/IHostBuilder.cs +++ b/src/WatsonWebserver.Core/IHostBuilder.cs @@ -1,9 +1,9 @@ -using System; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + /// /// Host builder interface. /// diff --git a/src/WatsonWebserver.Core/ISerializationHelper.cs b/src/WatsonWebserver.Core/ISerializationHelper.cs index d8d845b9..84e3b2fa 100644 --- a/src/WatsonWebserver.Core/ISerializationHelper.cs +++ b/src/WatsonWebserver.Core/ISerializationHelper.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Text; + /// /// Serialization helper. /// diff --git a/src/WatsonWebserver.Core/MimeTypes.cs b/src/WatsonWebserver.Core/MimeTypes.cs index a5e2b545..ad7efb71 100644 --- a/src/WatsonWebserver.Core/MimeTypes.cs +++ b/src/WatsonWebserver.Core/MimeTypes.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + /// /// MIME types and file extensions. /// diff --git a/src/WatsonWebserver.Core/ParameterRoute.cs b/src/WatsonWebserver.Core/ParameterRoute.cs index c88fcfe6..2d9f75a6 100644 --- a/src/WatsonWebserver.Core/ParameterRoute.cs +++ b/src/WatsonWebserver.Core/ParameterRoute.cs @@ -1,14 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using System.Text.Json; + using System.Text.Json.Serialization; + /// /// Assign a method handler for when requests are received matching the supplied method and path containing parameters. /// diff --git a/src/WatsonWebserver.Core/ParameterRouteManager.cs b/src/WatsonWebserver.Core/ParameterRouteManager.cs index c9c38081..705acad5 100644 --- a/src/WatsonWebserver.Core/ParameterRouteManager.cs +++ b/src/WatsonWebserver.Core/ParameterRouteManager.cs @@ -1,14 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using UrlMatcher; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Linq; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using UrlMatcher; + /// /// Parameter route manager. Parameter routes are used for requests using any HTTP method to any path where parameters are defined in the URL. /// For example, /{version}/api. diff --git a/src/WatsonWebserver.Core/QueryDetails.cs b/src/WatsonWebserver.Core/QueryDetails.cs new file mode 100644 index 00000000..52bfe6b4 --- /dev/null +++ b/src/WatsonWebserver.Core/QueryDetails.cs @@ -0,0 +1,117 @@ +namespace WatsonWebserver.Core +{ + using System; + using System.Collections; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Text; + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Threading; + using System.Threading.Tasks; + using Timestamps; + + /// + /// Query details. + /// + public class QueryDetails + { + #region Public-Members + + /// + /// Querystring, excluding the leading '?'. + /// + public string Querystring + { + get + { + if (_FullUrl.Contains("?")) + { + return _FullUrl.Substring(_FullUrl.IndexOf("?") + 1, (_FullUrl.Length - _FullUrl.IndexOf("?") - 1)); + } + else + { + return null; + } + } + } + + /// + /// Query elements. + /// + public NameValueCollection Elements + { + get + { + NameValueCollection ret = new NameValueCollection(StringComparer.InvariantCultureIgnoreCase); + string qs = Querystring; + if (!String.IsNullOrEmpty(qs)) + { + string[] queries = qs.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries); + if (queries.Length > 0) + { + for (int i = 0; i < queries.Length; i++) + { + string[] queryParts = queries[i].Split('='); + if (queryParts != null && queryParts.Length == 2) + { + ret.Add(queryParts[0], queryParts[1]); + } + else if (queryParts != null && queryParts.Length == 1) + { + ret.Add(queryParts[0], null); + } + } + } + } + + return ret; + } + } + + #endregion + + #region Private-Members + + private string _FullUrl = null; + + #endregion + + #region Constructors-and-Factories + + /// + /// Query details. + /// + public QueryDetails() + { + + } + + /// + /// Query details. + /// + /// Full URL. + public QueryDetails(string fullUrl) + { + if (String.IsNullOrEmpty(fullUrl)) throw new ArgumentNullException(nameof(fullUrl)); + + _FullUrl = fullUrl; + } + + #endregion + + #region Public-Methods + + #endregion + + #region Private-Methods + + #endregion + } +} diff --git a/src/WatsonWebserver.Core/RequestEventArgs.cs b/src/WatsonWebserver.Core/RequestEventArgs.cs index 27ddd994..d15ead42 100644 --- a/src/WatsonWebserver.Core/RequestEventArgs.cs +++ b/src/WatsonWebserver.Core/RequestEventArgs.cs @@ -1,10 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Text; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Text; + /// /// Request event arguments. /// diff --git a/src/WatsonWebserver.Core/ResponseEventArgs.cs b/src/WatsonWebserver.Core/ResponseEventArgs.cs index 166528bb..9a81ca68 100644 --- a/src/WatsonWebserver.Core/ResponseEventArgs.cs +++ b/src/WatsonWebserver.Core/ResponseEventArgs.cs @@ -1,10 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Text; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Text; + /// /// Response event arguments. /// diff --git a/src/WatsonWebserver.Core/RouteTypeEnum.cs b/src/WatsonWebserver.Core/RouteTypeEnum.cs index 8d2dc4d8..9080da14 100644 --- a/src/WatsonWebserver.Core/RouteTypeEnum.cs +++ b/src/WatsonWebserver.Core/RouteTypeEnum.cs @@ -1,7 +1,7 @@ -using System.Runtime.Serialization; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System.Runtime.Serialization; + /// /// Route type. /// diff --git a/src/WatsonWebserver.Core/RoutingGroup.cs b/src/WatsonWebserver.Core/RoutingGroup.cs index b1c942de..d1919d25 100644 --- a/src/WatsonWebserver.Core/RoutingGroup.cs +++ b/src/WatsonWebserver.Core/RoutingGroup.cs @@ -1,10 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Text; + using System.Threading.Tasks; + /// /// Routing group. /// diff --git a/src/WatsonWebserver.Core/SourceDetails.cs b/src/WatsonWebserver.Core/SourceDetails.cs new file mode 100644 index 00000000..f5e30259 --- /dev/null +++ b/src/WatsonWebserver.Core/SourceDetails.cs @@ -0,0 +1,90 @@ +namespace WatsonWebserver.Core +{ + using System; + using System.Collections; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Text; + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Threading; + using System.Threading.Tasks; + using Timestamps; + + /// + /// Source details. + /// + public class SourceDetails + { + #region Public-Members + + /// + /// IP address of the requestor. + /// + public string IpAddress { get; set; } = null; + + /// + /// TCP port from which the request originated on the requestor. + /// + public int Port + { + get + { + return _Port; + } + set + { + if (value < 0 || value > 65535) throw new ArgumentOutOfRangeException(nameof(Port)); + _Port = value; + } + } + + #endregion + + #region Private-Members + + private int _Port { get; set; } = 0; + + #endregion + + #region Constructors-and-Factories + + /// + /// Source details. + /// + public SourceDetails() + { + + } + + /// + /// Source details. + /// + /// IP address of the requestor. + /// TCP port from which the request originated on the requestor. + public SourceDetails(string ip, int port) + { + if (String.IsNullOrEmpty(ip)) throw new ArgumentNullException(nameof(ip)); + if (port < 0) throw new ArgumentOutOfRangeException(nameof(port)); + + IpAddress = ip; + Port = port; + } + + #endregion + + #region Public-Methods + + #endregion + + #region Private-Methods + + #endregion + } +} diff --git a/src/WatsonWebserver.Core/StaticRoute.cs b/src/WatsonWebserver.Core/StaticRoute.cs index 5136fa4b..d7886caf 100644 --- a/src/WatsonWebserver.Core/StaticRoute.cs +++ b/src/WatsonWebserver.Core/StaticRoute.cs @@ -1,13 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using System.Text.Json; + using System.Text.Json.Serialization; + /// /// Assign a method handler for when requests are received matching the supplied method and path. /// diff --git a/src/WatsonWebserver.Core/StaticRouteManager.cs b/src/WatsonWebserver.Core/StaticRouteManager.cs index 5bffa52b..5e726d99 100644 --- a/src/WatsonWebserver.Core/StaticRouteManager.cs +++ b/src/WatsonWebserver.Core/StaticRouteManager.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + /// /// Static route manager. Static routes are used for requests using any HTTP method to a specific path. /// diff --git a/src/WatsonWebserver.Core/UrlDetails.cs b/src/WatsonWebserver.Core/UrlDetails.cs new file mode 100644 index 00000000..f82c97f2 --- /dev/null +++ b/src/WatsonWebserver.Core/UrlDetails.cs @@ -0,0 +1,145 @@ +namespace WatsonWebserver.Core +{ + using System; + using System.Collections; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Text; + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Threading; + using System.Threading.Tasks; + using Timestamps; + + /// + /// URL details. + /// + public class UrlDetails + { + #region Public-Members + + /// + /// Full URL. + /// + public string Full { get; set; } = null; + + /// + /// Raw URL with query. + /// + public string RawWithQuery { get; set; } = null; + + /// + /// Raw URL without query. + /// + public string RawWithoutQuery + { + get + { + if (!String.IsNullOrEmpty(RawWithQuery)) + { + if (RawWithQuery.Contains("?")) return RawWithQuery.Substring(0, RawWithQuery.IndexOf("?")); + else return RawWithQuery; + } + else + { + return null; + } + } + } + + /// + /// Raw URL elements. + /// + public string[] Elements + { + get + { + string rawUrl = RawWithoutQuery; + + if (!String.IsNullOrEmpty(rawUrl)) + { + while (rawUrl.Contains("//")) rawUrl = rawUrl.Replace("//", "/"); + while (rawUrl.StartsWith("/")) rawUrl = rawUrl.Substring(1); + while (rawUrl.EndsWith("/")) rawUrl = rawUrl.Substring(0, rawUrl.Length - 1); + string[] encoded = rawUrl.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + if (encoded != null && encoded.Length > 0) + { + string[] decoded = new string[encoded.Length]; + for (int i = 0; i < encoded.Length; i++) + { + decoded[i] = WebUtility.UrlDecode(encoded[i]); + } + + return decoded; + } + } + + string[] ret = new string[0]; + return ret; + } + } + + /// + /// Parameters found within the URL, if using parameter routes. + /// + public NameValueCollection Parameters + { + get + { + return _Parameters; + } + set + { + if (value == null) _Parameters = new NameValueCollection(StringComparer.InvariantCultureIgnoreCase); + else _Parameters = value; + } + } + + #endregion + + #region Private-Members + + private NameValueCollection _Parameters = new NameValueCollection(StringComparer.InvariantCultureIgnoreCase); + + #endregion + + #region Constructors-and-Factories + + /// + /// URL details. + /// + public UrlDetails() + { + + } + + /// + /// URL details. + /// + /// Full URL. + /// Raw URL. + public UrlDetails(string fullUrl, string rawUrl) + { + if (String.IsNullOrEmpty(rawUrl)) throw new ArgumentNullException(nameof(rawUrl)); + + Full = fullUrl; + RawWithQuery = rawUrl; + } + + #endregion + + #region Public-Methods + + #endregion + + #region Private-Methods + + #endregion + } +} diff --git a/src/WatsonWebserver.Core/WatsonWebserver.Core.csproj b/src/WatsonWebserver.Core/WatsonWebserver.Core.csproj index 4f6c3514..797b5844 100644 --- a/src/WatsonWebserver.Core/WatsonWebserver.Core.csproj +++ b/src/WatsonWebserver.Core/WatsonWebserver.Core.csproj @@ -1,8 +1,8 @@  - netstandard2.1;net462;net48;net6.0;net7.0;net8.0 - 6.2.3 + netstandard2.1;net462;net48;net6.0;net8.0 + 6.3.1 Core library for Watson and Watson.Lite; simple, fast, async C# web servers for handling REST requests with SSL support, targeted to .NET Core, .NET Standard, and .NET Framework. Dependency update diff --git a/src/WatsonWebserver.Core/WatsonWebserver.Core.xml b/src/WatsonWebserver.Core/WatsonWebserver.Core.xml index 059db9c6..fe334dbb 100644 --- a/src/WatsonWebserver.Core/WatsonWebserver.Core.xml +++ b/src/WatsonWebserver.Core/WatsonWebserver.Core.xml @@ -247,6 +247,44 @@ Pretty print. JSON. + + + Destination details. + + + + + IP address to which the request was made. + + + + + TCP port on which the request was received. + + + + + Hostname to which the request was directed. + + + + + Hostname elements. + + + + + Destination details. + + + + + Source details. + + IP address to which the request was made. + TCP port on which the request was received. + Hostname. + Assign a method handler for when requests are received matching the supplied method and path regex. @@ -662,139 +700,6 @@ Key. Value. - - - Source details. - - - - - IP address of the requestor. - - - - - TCP port from which the request originated on the requestor. - - - - - Source details. - - - - - Source details. - - IP address of the requestor. - TCP port from which the request originated on the requestor. - - - - Destination details. - - - - - IP address to which the request was made. - - - - - TCP port on which the request was received. - - - - - Hostname to which the request was directed. - - - - - Hostname elements. - - - - - Destination details. - - - - - Source details. - - IP address to which the request was made. - TCP port on which the request was received. - Hostname. - - - - URL details. - - - - - Full URL. - - - - - Raw URL with query. - - - - - Raw URL without query. - - - - - Raw URL elements. - - - - - Parameters found within the URL, if using parameter routes. - - - - - URL details. - - - - - URL details. - - Full URL. - Raw URL. - - - - Query details. - - - - - Querystring, excluding the leading '?'. - - - - - Query elements. - - - - - Query details. - - - - - Query details. - - Full URL. - HTTP response. @@ -840,6 +745,11 @@ Indicates whether or not chunked transfer encoding should be indicated in the response. + + + Indicates whether or not server-sent events should be indicated in the response. + + Retrieve the response body sent using a Send() or SendAsync() method. @@ -900,19 +810,24 @@ Cancellation token useful for canceling the request. True if successful. - + - Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact. + Send headers (if not already sent) and a chunk of data using chunked transfer-encoding. + The connection will be kept in-tact unless isFinal is set to true. Chunk of data. + Boolean indicating if this is the final chunk. Cancellation token useful for canceling the request. True if successful. - + - Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection. + Send headers (if not already sent) and a server-sent event. + Watson will handle prepending 'data: ' to your event data. + The connection will be kept in-tact unless isFinal is set to true. - Chunk of data. + Event data. + Boolean indicating if this is the final chunk. Cancellation token useful for canceling the request. True if successful. @@ -1152,6 +1067,32 @@ Matching route. True if match exists. + + + Query details. + + + + + Querystring, excluding the leading '?'. + + + + + Query elements. + + + + + Query details. + + + + + Query details. + + Full URL. + Request event arguments. @@ -1325,6 +1266,33 @@ Instantiate. + + + Source details. + + + + + IP address of the requestor. + + + + + TCP port from which the request originated on the requestor. + + + + + Source details. + + + + + Source details. + + IP address of the requestor. + TCP port from which the request originated on the requestor. + Assign a method handler for when requests are received matching the supplied method and path. @@ -1424,6 +1392,48 @@ Matching route. Method to invoke. + + + URL details. + + + + + Full URL. + + + + + Raw URL with query. + + + + + Raw URL without query. + + + + + Raw URL elements. + + + + + Parameters found within the URL, if using parameter routes. + + + + + URL details. + + + + + URL details. + + Full URL. + Raw URL. + Webserver base. diff --git a/src/WatsonWebserver.Core/WebserverBase.cs b/src/WatsonWebserver.Core/WebserverBase.cs index 2a1fb587..f2686d50 100644 --- a/src/WatsonWebserver.Core/WebserverBase.cs +++ b/src/WatsonWebserver.Core/WebserverBase.cs @@ -1,15 +1,15 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Text.Json.Serialization; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Linq; + using System.Net; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + using System.Text.Json.Serialization; + /// /// Webserver base. /// diff --git a/src/WatsonWebserver.Core/WebserverConstants.cs b/src/WatsonWebserver.Core/WebserverConstants.cs index a166b63e..0d178475 100644 --- a/src/WatsonWebserver.Core/WebserverConstants.cs +++ b/src/WatsonWebserver.Core/WebserverConstants.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Text; + /// /// Webserver constants. /// diff --git a/src/WatsonWebserver.Core/WebserverEvents.cs b/src/WatsonWebserver.Core/WebserverEvents.cs index aeb7e70f..1dda3d1f 100644 --- a/src/WatsonWebserver.Core/WebserverEvents.cs +++ b/src/WatsonWebserver.Core/WebserverEvents.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Text; + /// /// Callbacks/actions to use when various events are encountered. /// diff --git a/src/WatsonWebserver.Core/WebserverPages.cs b/src/WatsonWebserver.Core/WebserverPages.cs index d2450d06..e86a243e 100644 --- a/src/WatsonWebserver.Core/WebserverPages.cs +++ b/src/WatsonWebserver.Core/WebserverPages.cs @@ -1,9 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Text; + /// /// Default pages served. /// diff --git a/src/WatsonWebserver.Core/WebserverRoutes.cs b/src/WatsonWebserver.Core/WebserverRoutes.cs index 8808eb20..d78e842e 100644 --- a/src/WatsonWebserver.Core/WebserverRoutes.cs +++ b/src/WatsonWebserver.Core/WebserverRoutes.cs @@ -1,10 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Text; + using System.Threading.Tasks; + /// /// Route manager. /// diff --git a/src/WatsonWebserver.Core/WebserverSettings.cs b/src/WatsonWebserver.Core/WebserverSettings.cs index 106980bc..d6a15e5d 100644 --- a/src/WatsonWebserver.Core/WebserverSettings.cs +++ b/src/WatsonWebserver.Core/WebserverSettings.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography.X509Certificates; -using System.Text; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.IO; + using System.Security.Cryptography.X509Certificates; + using System.Text; + /// /// Webserver settings. /// diff --git a/src/WatsonWebserver.Core/WebserverStatistics.cs b/src/WatsonWebserver.Core/WebserverStatistics.cs index 9a5836a6..4b35d756 100644 --- a/src/WatsonWebserver.Core/WebserverStatistics.cs +++ b/src/WatsonWebserver.Core/WebserverStatistics.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace WatsonWebserver.Core +namespace WatsonWebserver.Core { + using System; + using System.Collections.Generic; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + /// /// Webserver statistics. /// diff --git a/src/WatsonWebserver.Lite/Extensions/HostBuilderExtension/HostBuilder.cs b/src/WatsonWebserver.Lite/Extensions/HostBuilderExtension/HostBuilder.cs index 46d21b8d..a9d07759 100644 --- a/src/WatsonWebserver.Lite/Extensions/HostBuilderExtension/HostBuilder.cs +++ b/src/WatsonWebserver.Lite/Extensions/HostBuilderExtension/HostBuilder.cs @@ -1,10 +1,10 @@ -using System; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using WatsonWebserver.Core; - -namespace WatsonWebserver.Lite.Extensions.HostBuilderExtension +namespace WatsonWebserver.Lite.Extensions.HostBuilderExtension { + using System; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using WatsonWebserver.Core; + /// /// Host builder. /// diff --git a/src/WatsonWebserver.Lite/HttpContext.cs b/src/WatsonWebserver.Lite/HttpContext.cs index 9ad59791..66987457 100644 --- a/src/WatsonWebserver.Lite/HttpContext.cs +++ b/src/WatsonWebserver.Lite/HttpContext.cs @@ -1,14 +1,14 @@ -using System; -using System.IO; -using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; -using CavemanTcp; -using Timestamps; -using WatsonWebserver.Core; - -namespace WatsonWebserver.Lite +namespace WatsonWebserver.Lite { + using System; + using System.IO; + using System.Net; + using System.Text.Json; + using System.Text.Json.Serialization; + using CavemanTcp; + using Timestamps; + using WatsonWebserver.Core; + /// /// HTTP context including both request and response. /// diff --git a/src/WatsonWebserver.Lite/HttpRequest.cs b/src/WatsonWebserver.Lite/HttpRequest.cs index 7fa88712..62301f0c 100644 --- a/src/WatsonWebserver.Lite/HttpRequest.cs +++ b/src/WatsonWebserver.Lite/HttpRequest.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Specialized; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Text; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using CavemanTcp; -using Timestamps; -using WatsonWebserver.Core; - -namespace WatsonWebserver.Lite +namespace WatsonWebserver.Lite { + using System; + using System.Collections.Specialized; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Text.Json.Serialization; + using System.Threading; + using System.Threading.Tasks; + using CavemanTcp; + using Timestamps; + using WatsonWebserver.Core; + /// /// Data extracted from an incoming HTTP request. /// @@ -423,7 +423,7 @@ private byte[] ReadStream(Stream input, long contentLength) { if (input == null) throw new ArgumentNullException(nameof(input)); if (!input.CanRead) throw new InvalidOperationException("Input stream is not readable"); - if (contentLength < 1) return new byte[0]; + if (contentLength < 1) return Array.Empty(); byte[] buffer = new byte[_StreamBufferSize]; long bytesRemaining = contentLength; diff --git a/src/WatsonWebserver.Lite/HttpResponse.cs b/src/WatsonWebserver.Lite/HttpResponse.cs index 49b41387..33ec7bea 100644 --- a/src/WatsonWebserver.Lite/HttpResponse.cs +++ b/src/WatsonWebserver.Lite/HttpResponse.cs @@ -1,22 +1,22 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using System.Xml.Linq; -using CavemanTcp; -using WatsonWebserver.Core; - -namespace WatsonWebserver.Lite +namespace WatsonWebserver.Lite { + using System; + using System.Collections; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Text; + using System.Text.Json.Serialization; + using System.Threading; + using System.Threading.Tasks; + using System.Xml.Linq; + using CavemanTcp; + using WatsonWebserver.Core; + /// /// Response to an HTTP request. /// @@ -127,21 +127,14 @@ internal HttpResponse( #region Public-Methods - /// - /// Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for HEAD requests where the content length must be set. - /// - /// Cancellation token for canceling the request. + /// public override async Task Send(CancellationToken token = default) { if (ChunkedTransfer) throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); return await SendInternalAsync(0, null, true, token).ConfigureAwait(false); } - /// - /// Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for HEAD requests where the content length must be set. - /// - /// Value to set in Content-Length header. - /// Cancellation token for canceling the request. + /// public override async Task Send(long contentLength, CancellationToken token = default) { if (ChunkedTransfer) throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); @@ -149,11 +142,7 @@ public override async Task Send(long contentLength, CancellationToken toke return await SendInternalAsync(0, null, true, token).ConfigureAwait(false); } - /// - /// Send headers and data to the requestor and terminate the connection. - /// - /// Data. - /// Cancellation token for canceling the request. + /// public override async Task Send(string data, CancellationToken token = default) { if (ChunkedTransfer) throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); @@ -167,11 +156,7 @@ public override async Task Send(string data, CancellationToken token = def return await SendInternalAsync(bytes.Length, ms, true, token).ConfigureAwait(false); } - /// - /// Send headers and data to the requestor and terminate the connection. - /// - /// Data. - /// Cancellation token for canceling the request. + /// public override async Task Send(byte[] data, CancellationToken token = default) { if (ChunkedTransfer) throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); @@ -184,12 +169,7 @@ public override async Task Send(byte[] data, CancellationToken token = def return await SendInternalAsync(data.Length, ms, true, token).ConfigureAwait(false); } - /// - /// Send headers and data to the requestor and terminate the connection. - /// - /// Number of bytes to read from the stream. - /// Stream containing response data. - /// Cancellation token for canceling the request. + /// public override async Task Send(long contentLength, Stream stream, CancellationToken token = default) { if (ChunkedTransfer) throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); @@ -199,13 +179,8 @@ public override async Task Send(long contentLength, Stream stream, Cancell return await SendInternalAsync(contentLength, stream, true, token).ConfigureAwait(false); } - /// - /// Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact. - /// - /// Chunk of data. - /// Cancellation token useful for canceling the request. - /// True if successful. - public override async Task SendChunk(byte[] chunk, CancellationToken token = default) + /// + public override async Task SendChunk(byte[] chunk, bool isFinal, CancellationToken token = default) { if (!ChunkedTransfer) throw new IOException("Response is not configured to use chunked transfer-encoding. Set ChunkedTransfer to true first, otherwise use Send()."); if (!_HeadersSet) SetDefaultHeaders(); @@ -215,22 +190,16 @@ public override async Task SendChunk(byte[] chunk, CancellationToken token try { - if (chunk == null || chunk.Length < 1) chunk = new byte[0]; - - byte[] chunkBytes = new byte[0]; + if (chunk == null || chunk.Length < 1) chunk = Array.Empty(); using (MemoryStream ms = new MemoryStream()) { - chunkBytes = AppendBytes(chunkBytes, Encoding.UTF8.GetBytes(Convert.ToString(chunk.Length, 16))); - chunkBytes = AppendBytes(chunkBytes, Encoding.UTF8.GetBytes("\r\n")); - - chunkBytes = AppendBytes(chunkBytes, chunk); - chunkBytes = AppendBytes(chunkBytes, Encoding.UTF8.GetBytes("\r\n")); - - await ms.WriteAsync(chunkBytes, 0, chunkBytes.Length, token).ConfigureAwait(false); + byte[] message = AppendBytes(Encoding.UTF8.GetBytes(chunk.Length.ToString("X") + "\r\n"), chunk); + message = AppendBytes(message, Encoding.UTF8.GetBytes("\r\n")); + if (isFinal) message = AppendBytes(message, Encoding.UTF8.GetBytes("0\r\n\r\n")); + await ms.WriteAsync(message, 0, message.Length, token).ConfigureAwait(false); ms.Seek(0, SeekOrigin.Begin); - - await SendInternalAsync(chunkBytes.Length, ms, false, token).ConfigureAwait(false); + await SendInternalAsync(message.Length, ms, isFinal, token).ConfigureAwait(false); } } catch (Exception) @@ -241,45 +210,40 @@ public override async Task SendChunk(byte[] chunk, CancellationToken token return true; } - /// - /// Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection. - /// - /// Chunk of data. - /// Cancellation token useful for canceling the request. - /// True if successful. - public override async Task SendFinalChunk(byte[] chunk, CancellationToken token = default) + /// + public override async Task SendEvent(string eventData, bool isFinal, CancellationToken token = default) { - if (!ChunkedTransfer) throw new IOException("Response is not configured to use chunked transfer-encoding. Set ChunkedTransfer to true first, otherwise use Send()."); + if (!ServerSentEvents) throw new IOException("Response is not configured to use server-sent events. Set ServerSentEvents to true first, otherwise use Send()."); if (!_HeadersSet) SetDefaultHeaders(); - if (chunk != null && chunk.Length > 0) - ContentLength += chunk.Length; + if (!String.IsNullOrEmpty(eventData)) + ContentLength += eventData.Length; try { - if (chunk == null || chunk.Length < 1) chunk = new byte[0]; + if (String.IsNullOrEmpty(eventData)) eventData = ""; + + string dataLine = "data: " + eventData + "\n\n"; + + byte[] message = Encoding.UTF8.GetBytes( + Encoding.UTF8.GetBytes(dataLine).Length.ToString("X") + + "\r\n" + + dataLine + + "\r\n"); using (MemoryStream ms = new MemoryStream()) { - byte[] chunkBytes = AppendBytes(new byte[0], new byte[0]); + await ms.WriteAsync(message, 0, message.Length, token).ConfigureAwait(false); - if (chunk.Length > 0) + if (isFinal) { - chunkBytes = AppendBytes(chunkBytes, Encoding.UTF8.GetBytes(Convert.ToString(chunk.Length, 16))); - chunkBytes = AppendBytes(chunkBytes, Encoding.UTF8.GetBytes("\r\n")); - - chunkBytes = AppendBytes(chunkBytes, chunk); - chunkBytes = AppendBytes(chunkBytes, Encoding.UTF8.GetBytes("\r\n")); + byte[] finalBytes = Encoding.UTF8.GetBytes("0\r\n\r\n"); + await ms.WriteAsync(finalBytes, 0, finalBytes.Length, token).ConfigureAwait(false); } - chunkBytes = AppendBytes(chunkBytes, Encoding.UTF8.GetBytes("0")); - chunkBytes = AppendBytes(chunkBytes, Encoding.UTF8.GetBytes("\r\n")); - chunkBytes = AppendBytes(chunkBytes, Encoding.UTF8.GetBytes("\r\n")); - - await ms.WriteAsync(chunkBytes, 0, chunkBytes.Length, token).ConfigureAwait(false); ms.Seek(0, SeekOrigin.Begin); - - await SendInternalAsync(chunkBytes.Length, ms, true, token).ConfigureAwait(false); + byte[] bytes = ms.ToArray(); + await SendInternalAsync(bytes.Length, ms, isFinal, token).ConfigureAwait(false); } } catch (Exception) @@ -305,7 +269,7 @@ public void Close() private byte[] GetHeaderBytes() { - byte[] ret = new byte[0]; + byte[] ret = Array.Empty(); ret = AppendBytes(ret, Encoding.UTF8.GetBytes(ProtocolVersion + " " + StatusCode + " " + GetStatusDescription(StatusCode) + "\r\n")); @@ -504,21 +468,34 @@ private string GetStatusDescription(int statusCode) private void SetDefaultHeaders() { - if (_HeaderSettings != null && Headers != null) + if (!_HeadersSet) { - foreach (KeyValuePair defaultHeader in _HeaderSettings.DefaultHeaders) + if (ChunkedTransfer || ServerSentEvents) + Headers.Add("Transfer-Encoding", "chunked"); + + if (ServerSentEvents) { - string key = defaultHeader.Key; - string val = defaultHeader.Value; + Headers.Add("Content-Type", "text/event-stream; charset=utf-8"); + Headers.Add("Cache-Control", "no-cache"); + Headers.Add("Connection", "keep-alive"); + } - if (!Headers.AllKeys.Any(k => k.ToLower().Equals(key.ToLower()))) + if (_HeaderSettings != null && Headers != null) + { + foreach (KeyValuePair defaultHeader in _HeaderSettings.DefaultHeaders) { - Headers.Add(key, val); + string key = defaultHeader.Key; + string val = defaultHeader.Value; + + if (!Headers.AllKeys.Any(k => k.ToLower().Equals(key.ToLower()))) + { + Headers.Add(key, val); + } } } - } - _HeadersSet = true; + _HeadersSet = true; + } } private byte[] ReadStreamFully(Stream input) @@ -544,7 +521,8 @@ private byte[] ReadStreamFully(Stream input) private void SetContentLength(long contentLength) { if (_HeaderSettings.IncludeContentLength - && !ChunkedTransfer) + && !ChunkedTransfer + && !ServerSentEvents) { if (Headers.Count > 0) { diff --git a/src/WatsonWebserver.Lite/WatsonWebserver.Lite.csproj b/src/WatsonWebserver.Lite/WatsonWebserver.Lite.csproj index 5c75e40b..70261e65 100644 --- a/src/WatsonWebserver.Lite/WatsonWebserver.Lite.csproj +++ b/src/WatsonWebserver.Lite/WatsonWebserver.Lite.csproj @@ -1,8 +1,8 @@  - netstandard2.1;net462;net48;net6.0;net7.0;net8.0 - 6.2.3 + netstandard2.1;net462;net48;net6.0;net8.0 + 6.3.0 Simple, fast, async C# web server for handling REST requests with SSL support, targeted to .NET Core, .NET Standard, and .NET Framework. Watson.Lite has no dependency on http.sys. Dependency update Joel Christner @@ -29,7 +29,7 @@ - + diff --git a/src/WatsonWebserver.Lite/WatsonWebserver.Lite.xml b/src/WatsonWebserver.Lite/WatsonWebserver.Lite.xml index 6987d650..88e2155c 100644 --- a/src/WatsonWebserver.Lite/WatsonWebserver.Lite.xml +++ b/src/WatsonWebserver.Lite/WatsonWebserver.Lite.xml @@ -236,55 +236,25 @@ - - Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for HEAD requests where the content length must be set. - - Cancellation token for canceling the request. + - - Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for HEAD requests where the content length must be set. - - Value to set in Content-Length header. - Cancellation token for canceling the request. + - - Send headers and data to the requestor and terminate the connection. - - Data. - Cancellation token for canceling the request. + - - Send headers and data to the requestor and terminate the connection. - - Data. - Cancellation token for canceling the request. + - - Send headers and data to the requestor and terminate the connection. - - Number of bytes to read from the stream. - Stream containing response data. - Cancellation token for canceling the request. + - - - Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact. - - Chunk of data. - Cancellation token useful for canceling the request. - True if successful. + + - - - Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection. - - Chunk of data. - Cancellation token useful for canceling the request. - True if successful. + + diff --git a/src/WatsonWebserver.Lite/Webserver.cs b/src/WatsonWebserver.Lite/Webserver.cs index 0210a17e..b332a41a 100644 --- a/src/WatsonWebserver.Lite/Webserver.cs +++ b/src/WatsonWebserver.Lite/Webserver.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using CavemanTcp; -using WatsonWebserver.Core; - -namespace WatsonWebserver.Lite +namespace WatsonWebserver.Lite { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.IO; + using System.Linq; + using System.Net; + using System.Reflection; + using System.Security.Cryptography.X509Certificates; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using CavemanTcp; + using WatsonWebserver.Core; + /// /// HttpServerLite web server. /// @@ -699,7 +699,7 @@ private async void ClientConnected(object sender, ClientConnectedEventArgs args) ctx.Response.StatusCode = 404; ctx.Response.ContentType = DefaultPages.Pages[404].ContentType; if (ctx.Response.ChunkedTransfer) - await ctx.Response.SendFinalChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[404].Content), _Token).ConfigureAwait(false); + await ctx.Response.SendChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[404].Content), true, _Token).ConfigureAwait(false); else await ctx.Response.Send(DefaultPages.Pages[404].Content, _Token).ConfigureAwait(false); return; @@ -717,7 +717,7 @@ private async void ClientConnected(object sender, ClientConnectedEventArgs args) try { if (ctx.Response.ChunkedTransfer) - await ctx.Response.SendFinalChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[500].Content), _Token).ConfigureAwait(false); + await ctx.Response.SendChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[500].Content), true, _Token).ConfigureAwait(false); else await ctx.Response.Send(DefaultPages.Pages[500].Content, _Token).ConfigureAwait(false); } @@ -744,7 +744,7 @@ private async void ClientConnected(object sender, ClientConnectedEventArgs args) ctx.Response.StatusCode = 500; ctx.Response.ContentType = DefaultPages.Pages[500].ContentType; if (ctx.Response.ChunkedTransfer) - await ctx.Response.SendFinalChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[500].Content)).ConfigureAwait(false); + await ctx.Response.SendChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[500].Content), true, _Token).ConfigureAwait(false); else await ctx.Response.Send(DefaultPages.Pages[500].Content).ConfigureAwait(false); } diff --git a/src/WatsonWebserver.sln b/src/WatsonWebserver.sln index b36dfe6e..975dd9d5 100644 --- a/src/WatsonWebserver.sln +++ b/src/WatsonWebserver.sln @@ -33,6 +33,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test.HeadResponse", "Test.H EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.Authentication", "Test.Authentication\Test.Authentication.csproj", "{866337D0-67F9-48AE-97F7-9AFBFEFB385F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test.ServerSentEvents", "Test.ServerSentEvents\Test.ServerSentEvents.csproj", "{A84FF99F-A011-4BC2-A093-59A302D45D5B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -99,6 +101,10 @@ Global {866337D0-67F9-48AE-97F7-9AFBFEFB385F}.Debug|Any CPU.Build.0 = Debug|Any CPU {866337D0-67F9-48AE-97F7-9AFBFEFB385F}.Release|Any CPU.ActiveCfg = Release|Any CPU {866337D0-67F9-48AE-97F7-9AFBFEFB385F}.Release|Any CPU.Build.0 = Release|Any CPU + {A84FF99F-A011-4BC2-A093-59A302D45D5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A84FF99F-A011-4BC2-A093-59A302D45D5B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A84FF99F-A011-4BC2-A093-59A302D45D5B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A84FF99F-A011-4BC2-A093-59A302D45D5B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/WatsonWebserver/Extensions/HostBuilderExtension/HostBuilder.cs b/src/WatsonWebserver/Extensions/HostBuilderExtension/HostBuilder.cs index 5d69d645..88d7228c 100644 --- a/src/WatsonWebserver/Extensions/HostBuilderExtension/HostBuilder.cs +++ b/src/WatsonWebserver/Extensions/HostBuilderExtension/HostBuilder.cs @@ -1,11 +1,11 @@ -using System; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using WatsonWebserver.Core; - -namespace WatsonWebserver.Extensions.HostBuilderExtension +namespace WatsonWebserver.Extensions.HostBuilderExtension { + using System; + using System.Runtime.InteropServices; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using WatsonWebserver.Core; + /// /// Host builder. /// diff --git a/src/WatsonWebserver/HttpContext.cs b/src/WatsonWebserver/HttpContext.cs index 0d282770..d4d485e7 100644 --- a/src/WatsonWebserver/HttpContext.cs +++ b/src/WatsonWebserver/HttpContext.cs @@ -1,13 +1,13 @@ -using System; -using System.IO; -using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; -using Timestamps; -using WatsonWebserver.Core; - -namespace WatsonWebserver +namespace WatsonWebserver { + using System; + using System.IO; + using System.Net; + using System.Text.Json; + using System.Text.Json.Serialization; + using Timestamps; + using WatsonWebserver.Core; + /// /// HTTP context including both request and response. /// diff --git a/src/WatsonWebserver/HttpRequest.cs b/src/WatsonWebserver/HttpRequest.cs index 636ded60..00aa533a 100644 --- a/src/WatsonWebserver/HttpRequest.cs +++ b/src/WatsonWebserver/HttpRequest.cs @@ -1,23 +1,23 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using Timestamps; -using WatsonWebserver.Core; - -namespace WatsonWebserver +namespace WatsonWebserver { + using System; + using System.Collections; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Text; + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Threading; + using System.Threading.Tasks; + using Timestamps; + using WatsonWebserver.Core; + /// /// HTTP request. /// diff --git a/src/WatsonWebserver/HttpResponse.cs b/src/WatsonWebserver/HttpResponse.cs index 989cd1ac..1c7c1f85 100644 --- a/src/WatsonWebserver/HttpResponse.cs +++ b/src/WatsonWebserver/HttpResponse.cs @@ -1,20 +1,20 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Text.Json; -using System.Text.Json.Serialization; -using WatsonWebserver.Core; - -namespace WatsonWebserver +namespace WatsonWebserver { + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using System.Text.Json; + using System.Text.Json.Serialization; + using WatsonWebserver.Core; + /// /// HTTP response. /// @@ -127,23 +127,14 @@ internal HttpResponse( #region Public-Methods - /// - /// Send headers and no data to the requestor and terminate the connection. - /// - /// Cancellation token useful for canceling the request. - /// True if successful. + /// public override async Task Send(CancellationToken token = default) { if (ChunkedTransfer) throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); return await SendInternalAsync(0, null, token).ConfigureAwait(false); } - /// - /// Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for HEAD requests where the content length must be set. - /// - /// Cancellation token useful for canceling the request. - /// Content length. - /// True if successful. + /// public override async Task Send(long contentLength, CancellationToken token = default) { if (ChunkedTransfer) throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); @@ -151,12 +142,7 @@ public override async Task Send(long contentLength, CancellationToken toke return await SendInternalAsync(0, null, token).ConfigureAwait(false); } - /// - /// Send headers and data to the requestor and terminate the connection. - /// - /// Data. - /// Cancellation token useful for canceling the request. - /// True if successful. + /// public override async Task Send(string data, CancellationToken token = default) { if (ChunkedTransfer) throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); @@ -170,12 +156,7 @@ public override async Task Send(string data, CancellationToken token = def return await SendInternalAsync(bytes.Length, ms, token).ConfigureAwait(false); } - /// - /// Send headers and data to the requestor and terminate the connection. - /// - /// Data. - /// Cancellation token useful for canceling the request. - /// True if successful. + /// public override async Task Send(byte[] data, CancellationToken token = default) { if (ChunkedTransfer) throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); @@ -188,26 +169,15 @@ public override async Task Send(byte[] data, CancellationToken token = def return await SendInternalAsync(data.Length, ms, token).ConfigureAwait(false); } - /// - /// Send headers and data to the requestor and terminate. - /// - /// Number of bytes to send. - /// Stream containing the data. - /// Cancellation token useful for canceling the request. - /// True if successful. + /// public override async Task Send(long contentLength, Stream stream, CancellationToken token = default) { if (ChunkedTransfer) throw new IOException("Response is configured to use chunked transfer-encoding. Use SendChunk() and SendFinalChunk()."); return await SendInternalAsync(contentLength, stream, token); } - /// - /// Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact. - /// - /// Chunk of data. - /// Cancellation token useful for canceling the request. - /// True if successful. - public override async Task SendChunk(byte[] chunk, CancellationToken token = default) + /// + public override async Task SendChunk(byte[] chunk, bool isFinal, CancellationToken token = default) { if (!ChunkedTransfer) throw new IOException("Response is not configured to use chunked transfer-encoding. Set ChunkedTransfer to true first, otherwise use Send()."); if (!_HeadersSet) SendHeaders(); @@ -220,6 +190,16 @@ public override async Task SendChunk(byte[] chunk, CancellationToken token if (chunk == null || chunk.Length < 1) chunk = Array.Empty(); await _OutputStream.WriteAsync(chunk, 0, chunk.Length, token).ConfigureAwait(false); await _OutputStream.FlushAsync(token).ConfigureAwait(false); + + if (isFinal) + { + byte[] endChunk = Array.Empty(); + await _OutputStream.WriteAsync(endChunk, 0, endChunk.Length, token).ConfigureAwait(false); + await _OutputStream.FlushAsync(token).ConfigureAwait(false); + _OutputStream.Close(); + if (_Response != null) _Response.Close(); + ResponseSent = true; + } } catch (Exception) { @@ -229,34 +209,33 @@ public override async Task SendChunk(byte[] chunk, CancellationToken token return true; } - /// - /// Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection. - /// - /// Chunk of data. - /// Cancellation token useful for canceling the request. - /// True if successful. - public override async Task SendFinalChunk(byte[] chunk, CancellationToken token = default) + /// + public override async Task SendEvent(string eventData, bool isFinal, CancellationToken token = default) { - if (!ChunkedTransfer) throw new IOException("Response is not configured to use chunked transfer-encoding. Set ChunkedTransfer to true first, otherwise use Send()."); + if (!ServerSentEvents) throw new IOException("Response is not configured to use server-sent events. Set ServerSentEvents to true first, otherwise use Send()."); if (!_HeadersSet) SendHeaders(); - if (chunk != null && chunk.Length > 0) - ContentLength += chunk.Length; + if (!String.IsNullOrEmpty(eventData)) + ContentLength += eventData.Length; try - { - if (chunk != null && chunk.Length > 0) - await _OutputStream.WriteAsync(chunk, 0, chunk.Length, token).ConfigureAwait(false); - - byte[] endChunk = Array.Empty(); - await _OutputStream.WriteAsync(endChunk, 0, endChunk.Length, token).ConfigureAwait(false); + { + if (String.IsNullOrEmpty(eventData)) eventData = ""; + byte[] dataBytes = Encoding.UTF8.GetBytes("data: " + eventData + "\n\n"); + await _OutputStream.WriteAsync(dataBytes, 0, dataBytes.Length, token).ConfigureAwait(false); await _OutputStream.FlushAsync(token).ConfigureAwait(false); - _OutputStream.Close(); - if (_Response != null) _Response.Close(); + if (isFinal) + { + byte[] endChunk = Array.Empty(); + await _OutputStream.WriteAsync(endChunk, 0, endChunk.Length, token).ConfigureAwait(false); + await _OutputStream.FlushAsync(token).ConfigureAwait(false); + _OutputStream.Close(); + if (_Response != null) _Response.Close(); + ResponseSent = true; + } - ResponseSent = true; return true; } catch (Exception) @@ -421,10 +400,17 @@ private void SendHeaders() _Response.ContentLength64 = ContentLength; _Response.StatusCode = StatusCode; _Response.StatusDescription = GetStatusDescription(StatusCode); - _Response.SendChunked = ChunkedTransfer; + _Response.SendChunked = (ChunkedTransfer || ServerSentEvents); _Response.ContentType = ContentType; _Response.KeepAlive = false; + if (ServerSentEvents) + { + _Response.ContentType = "text/event-stream; charset=utf-8"; + _Response.Headers.Add("Cache-Control", "no-cache"); + _Response.Headers.Add("Connection", "keep-alive"); + } + if (Headers != null && Headers.Count > 0) { for (int i = 0; i < Headers.Count; i++) diff --git a/src/WatsonWebserver/WatsonWebserver.csproj b/src/WatsonWebserver/WatsonWebserver.csproj index 837710fe..ec8a57c0 100644 --- a/src/WatsonWebserver/WatsonWebserver.csproj +++ b/src/WatsonWebserver/WatsonWebserver.csproj @@ -1,8 +1,8 @@  - netstandard2.1;net462;net48;net6.0;net7.0;net8.0 - 6.2.3 + netstandard2.1;net462;net48;net6.0;net8.0 + 6.3.0 Simple, fast, async C# web server for handling REST requests with SSL support, targeted to .NET Core, .NET Standard, and .NET Framework. Dependency update Joel Christner @@ -27,10 +27,6 @@ WatsonWebserver.xml - - - - True @@ -47,6 +43,10 @@ + + + + Always diff --git a/src/WatsonWebserver/WatsonWebserver.xml b/src/WatsonWebserver/WatsonWebserver.xml index 9a6b8ba5..fbd7567b 100644 --- a/src/WatsonWebserver/WatsonWebserver.xml +++ b/src/WatsonWebserver/WatsonWebserver.xml @@ -249,60 +249,25 @@ - - Send headers and no data to the requestor and terminate the connection. - - Cancellation token useful for canceling the request. - True if successful. + - - Send headers with a specified content length and no data to the requestor and terminate the connection. Useful for HEAD requests where the content length must be set. - - Cancellation token useful for canceling the request. - Content length. - True if successful. + - - Send headers and data to the requestor and terminate the connection. - - Data. - Cancellation token useful for canceling the request. - True if successful. + - - Send headers and data to the requestor and terminate the connection. - - Data. - Cancellation token useful for canceling the request. - True if successful. + - - Send headers and data to the requestor and terminate. - - Number of bytes to send. - Stream containing the data. - Cancellation token useful for canceling the request. - True if successful. + - - - Send headers (if not already sent) and a chunk of data using chunked transfer-encoding, and keep the connection in-tact. - - Chunk of data. - Cancellation token useful for canceling the request. - True if successful. + + - - - Send headers (if not already sent) and the final chunk of data using chunked transfer-encoding and terminate the connection. - - Chunk of data. - Cancellation token useful for canceling the request. - True if successful. + + diff --git a/src/WatsonWebserver/Webserver.cs b/src/WatsonWebserver/Webserver.cs index 9c5b8a91..051d6ee6 100644 --- a/src/WatsonWebserver/Webserver.cs +++ b/src/WatsonWebserver/Webserver.cs @@ -1,18 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Linq; -using System.Net; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Text.Json.Serialization; -using WatsonWebserver.Core; -using System.Runtime.InteropServices; -using System.Text; - -namespace WatsonWebserver +namespace WatsonWebserver { + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Linq; + using System.Net; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + using System.Text.Json.Serialization; + using WatsonWebserver.Core; + using System.Runtime.InteropServices; + using System.Text; + /// /// Watson webserver. /// @@ -636,7 +636,7 @@ private async Task AcceptConnections(CancellationToken token) ctx.Response.StatusCode = 404; ctx.Response.ContentType = DefaultPages.Pages[404].ContentType; if (ctx.Response.ChunkedTransfer) - await ctx.Response.SendFinalChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[404].Content)).ConfigureAwait(false); + await ctx.Response.SendChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[404].Content), true).ConfigureAwait(false); else await ctx.Response.Send(DefaultPages.Pages[404].Content).ConfigureAwait(false); return; @@ -649,7 +649,7 @@ private async Task AcceptConnections(CancellationToken token) ctx.Response.StatusCode = 500; ctx.Response.ContentType = DefaultPages.Pages[500].ContentType; if (ctx.Response.ChunkedTransfer) - await ctx.Response.SendFinalChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[500].Content)).ConfigureAwait(false); + await ctx.Response.SendChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[500].Content), true).ConfigureAwait(false); else await ctx.Response.Send(DefaultPages.Pages[500].Content).ConfigureAwait(false); Events.HandleExceptionEncountered(this, new ExceptionEventArgs(ctx, eInner)); @@ -665,7 +665,7 @@ private async Task AcceptConnections(CancellationToken token) ctx.Response.StatusCode = 500; ctx.Response.ContentType = DefaultPages.Pages[500].ContentType; if (ctx.Response.ChunkedTransfer) - await ctx.Response.SendFinalChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[500].Content)).ConfigureAwait(false); + await ctx.Response.SendChunk(Encoding.UTF8.GetBytes(DefaultPages.Pages[500].Content), true).ConfigureAwait(false); else await ctx.Response.Send(DefaultPages.Pages[500].Content).ConfigureAwait(false); }