From 19d487683468db5b7e34d449ac105b088cef9b5a Mon Sep 17 00:00:00 2001 From: Michael Becker Date: Sun, 5 Jan 2025 23:39:00 -0500 Subject: [PATCH 1/3] play nice to allow for embedding into other host processes e.g. tests --- src/lib/MBS.Web/WebApplication.cs | 33 +++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/lib/MBS.Web/WebApplication.cs b/src/lib/MBS.Web/WebApplication.cs index 76f1c9a..269550c 100644 --- a/src/lib/MBS.Web/WebApplication.cs +++ b/src/lib/MBS.Web/WebApplication.cs @@ -8,6 +8,8 @@ namespace MBS.Web; public abstract class WebApplication : Application { protected abstract int DefaultPort { get; } + public int Port { get { return WebServer.EndPoint.Port; } } + public WebServer WebServer { get; private set; } public string VirtualBasePath { get; set; } = "/"; @@ -40,20 +42,39 @@ public abstract class WebApplication : Application return new WebServer(new IPEndPoint(IPAddress.Any, DefaultPort)); } + private bool shuttingDown = false; + protected override void StopInternal(int exitCode = 0) + { + shuttingDown = true; + if (!SupportEmbeddedHosting) + { + // we do not want to System.Environment.Exit() here + // if we are hosted in another process (e.g. for tests) + base.StopInternal(exitCode); + } + } + + public bool SupportEmbeddedHosting { get; set; } = false; + protected override void OnStartup(EventArgs e) { base.OnStartup(e); - WebServer server = CreateWebServer(); + WebServer = CreateWebServer(); Console.WriteLine("attempting to start server listening on port {0}", DefaultPort); - OnServerCreated(new WebServerCreatedEventArgs(server)); - server.ProcessRequest += server_OnProcessRequest; - server.Start(); + OnServerCreated(new WebServerCreatedEventArgs(WebServer)); + WebServer.ProcessRequest += server_OnProcessRequest; + WebServer.Start(); - while (true) + if (!SupportEmbeddedHosting) { - Thread.Sleep(50); + while (!shuttingDown) + { + Thread.Sleep(50); + } + + WebServer.Stop(); } } From 38bf02d4069dc984df33a05de8deb514c31038a1 Mon Sep 17 00:00:00 2001 From: Michael Becker Date: Fri, 17 Jan 2025 22:13:32 -0500 Subject: [PATCH 2/3] allow non-form-encoded content --- src/lib/MBS.Web/WebRequest.cs | 35 ++++++++++++++++++++++++++++++++++- src/lib/MBS.Web/WebServer.cs | 27 ++++++++++++++++++++------- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/lib/MBS.Web/WebRequest.cs b/src/lib/MBS.Web/WebRequest.cs index 2d6f4e9..21419c8 100644 --- a/src/lib/MBS.Web/WebRequest.cs +++ b/src/lib/MBS.Web/WebRequest.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using System.Data.SqlTypes; using System.Net; +using System.Text; using MBS.Core; namespace MBS.Web; @@ -21,8 +22,9 @@ public class WebRequest public WebHeaderCollection Headers { get; } public ReadOnlyDictionary Form { get; } + public string Content { get; } - public WebRequest(string version, string method, string path, WebHeaderCollection headers, Dictionary pathVariables, Dictionary form) + public WebRequest(string version, string method, string path, WebHeaderCollection headers, Dictionary pathVariables) { Version = version; Method = method; @@ -67,9 +69,40 @@ public class WebRequest } Headers = headers; PathVariables = pathVariables; + } + public WebRequest(string version, string method, string path, WebHeaderCollection headers, Dictionary pathVariables, string content) : this(version, method, path, headers, pathVariables) + { + Content = content; + Form = ReadOnlyDictionary.Empty; + } + public WebRequest(string version, string method, string path, WebHeaderCollection headers, Dictionary pathVariables, Dictionary form) : this(version, method, path, headers, pathVariables) + { + Content = UrlAndFormEncode(form); Form = new ReadOnlyDictionary(form); } + private string UrlAndFormEncode(string v) + { + v = v.UrlEncode(); + return v; + } + private string UrlAndFormEncode(IEnumerable> v) + { + StringBuilder sb = new StringBuilder(); + int i = 0; + foreach (KeyValuePair kvp in v) + { + sb.Append(UrlAndFormEncode(kvp.Key)); + sb.Append('='); + sb.Append(UrlAndFormEncode(kvp.Value)); + if (i < v.Count()) + { + sb.Append('&'); + } + i++; + } + return sb.ToString(); + } private string UrlAndFormDecode(string v) { v = v.Replace('+', ' '); // must be done first to not decode '%..' => ' ' diff --git a/src/lib/MBS.Web/WebServer.cs b/src/lib/MBS.Web/WebServer.cs index 00011a6..eb60042 100644 --- a/src/lib/MBS.Web/WebServer.cs +++ b/src/lib/MBS.Web/WebServer.cs @@ -257,6 +257,11 @@ public class WebServer Dictionary form = new Dictionary(); string contentLength = headers["Content-Length"]; + string contentType = headers["Content-Type"]; + + // I never thought I'd ever actually use this! + Union> content; + if (contentLength != "") { int contentLengthInt = Int32.Parse(contentLength); @@ -265,16 +270,24 @@ public class WebServer char[] buffer = new char[contentLengthInt]; sr.ReadBlock(buffer, 0, contentLengthInt); - string content = new string(buffer); - Console.Error.WriteLine(content); + string contentString = new string(buffer); + Console.Error.WriteLine(contentString); - if (!String.IsNullOrEmpty(content)) + if (!String.IsNullOrEmpty(contentString)) { - string[] kvps = content.Split(new char[] { '&' }); - foreach (string kvp in kvps) + if (contentType == "application/x-www-form-urlencoded") { - string[] pv = kvp.Split(new char[] { '=' }); - form[pv[0].UrlDecode()] = pv[1].UrlDecode().Replace('+', ' '); + string[] kvps = contentString.Split(new char[] { '&' }); + foreach (string kvp in kvps) + { + string[] pv = kvp.Split(new char[] { '=' }); + form[pv[0].UrlDecode()] = pv[1].UrlDecode().Replace('+', ' '); + } + content = form; + } + else + { + content = contentString; } } } From 04dabc6455f074767828dc04ae8a47ca51a27008 Mon Sep 17 00:00:00 2001 From: Michael Becker Date: Thu, 6 Mar 2025 16:17:52 -0500 Subject: [PATCH 3/3] catch exceptions differently if we are debugging --- src/lib/MBS.Web/WebServer.cs | 66 ++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/src/lib/MBS.Web/WebServer.cs b/src/lib/MBS.Web/WebServer.cs index eb60042..4829259 100644 --- a/src/lib/MBS.Web/WebServer.cs +++ b/src/lib/MBS.Web/WebServer.cs @@ -1,4 +1,5 @@ -using System.Net; +using System.Diagnostics; +using System.Net; using System.Net.Security; using System.Net.Sockets; using System.Security.Authentication; @@ -386,27 +387,56 @@ public class WebServer { // Console.WriteLine("Client connected"); - try + if (Debugger.IsAttached) { - HandleClient(client); + // do not catch generic exceptions when running under a debugger + try + { + HandleClient(client); + } + catch (System.Net.Sockets.SocketException ex) + { + Console.Error.WriteLine("caught SocketException; ignoring"); + Console.Error.WriteLine(ex.Message); + } + catch (System.IO.IOException ex) + { + Console.Error.WriteLine("caught IOException; ignoring"); + Console.Error.WriteLine(ex.Message); + } + /* + catch (Exception ex) + { + Console.Error.WriteLine("caught exception of type {0}", ex.GetType().FullName); + Console.Error.WriteLine(ex.Message); + Console.Error.WriteLine(ex.StackTrace); + } + */ } - catch (System.Net.Sockets.SocketException ex) + else { - Console.Error.WriteLine("caught SocketException; ignoring"); - Console.Error.WriteLine(ex.Message); + // also catch generic exceptions if we are not debugging + try + { + HandleClient(client); + } + catch (System.Net.Sockets.SocketException ex) + { + Console.Error.WriteLine("caught SocketException; ignoring"); + Console.Error.WriteLine(ex.Message); + } + catch (System.IO.IOException ex) + { + Console.Error.WriteLine("caught IOException; ignoring"); + Console.Error.WriteLine(ex.Message); + } + catch (Exception ex) + { + Console.Error.WriteLine("caught exception of type {0}", ex.GetType().FullName); + Console.Error.WriteLine(ex.Message); + Console.Error.WriteLine(ex.StackTrace); + } } - catch (System.IO.IOException ex) - { - Console.Error.WriteLine("caught IOException; ignoring"); - Console.Error.WriteLine(ex.Message); - } - catch (Exception ex) - { - Console.Error.WriteLine("caught exception of type {0}", ex.GetType().FullName); - Console.Error.WriteLine(ex.Message); - Console.Error.WriteLine(ex.StackTrace); - } - client.Close(); } }