Merge branch 'master' of gitea.azcona-becker.net:alcetech/web-framework-dotnet

This commit is contained in:
Michael Becker 2025-03-20 18:50:48 -04:00
commit 732daae00e
3 changed files with 129 additions and 32 deletions

View File

@ -8,6 +8,8 @@ namespace MBS.Web;
public abstract class WebApplication : Application public abstract class WebApplication : Application
{ {
protected abstract int DefaultPort { get; } protected abstract int DefaultPort { get; }
public int Port { get { return WebServer.EndPoint.Port; } }
public WebServer WebServer { get; private set; }
public string VirtualBasePath { get; set; } = "/"; public string VirtualBasePath { get; set; } = "/";
@ -40,21 +42,40 @@ public abstract class WebApplication : Application
return new WebServer(new IPEndPoint(IPAddress.Any, DefaultPort)); 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) protected override void OnStartup(EventArgs e)
{ {
base.OnStartup(e); base.OnStartup(e);
WebServer server = CreateWebServer(); WebServer = CreateWebServer();
Console.WriteLine("attempting to start server listening on port {0}", DefaultPort); Console.WriteLine("attempting to start server listening on port {0}", DefaultPort);
OnServerCreated(new WebServerCreatedEventArgs(server)); OnServerCreated(new WebServerCreatedEventArgs(WebServer));
server.ProcessRequest += server_OnProcessRequest; WebServer.ProcessRequest += server_OnProcessRequest;
server.Start(); WebServer.Start();
while (true) if (!SupportEmbeddedHosting)
{
while (!shuttingDown)
{ {
Thread.Sleep(50); Thread.Sleep(50);
} }
WebServer.Stop();
}
} }
public virtual string ExpandRelativePath(string path) public virtual string ExpandRelativePath(string path)

View File

@ -1,6 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Data.SqlTypes; using System.Data.SqlTypes;
using System.Net; using System.Net;
using System.Text;
using MBS.Core; using MBS.Core;
namespace MBS.Web; namespace MBS.Web;
@ -21,8 +22,9 @@ public class WebRequest
public WebHeaderCollection Headers { get; } public WebHeaderCollection Headers { get; }
public ReadOnlyDictionary<string, string> Form { get; } public ReadOnlyDictionary<string, string> Form { get; }
public string Content { get; }
public WebRequest(string version, string method, string path, WebHeaderCollection headers, Dictionary<string, string> pathVariables, Dictionary<string, string> form) public WebRequest(string version, string method, string path, WebHeaderCollection headers, Dictionary<string, string> pathVariables)
{ {
Version = version; Version = version;
Method = method; Method = method;
@ -67,9 +69,40 @@ public class WebRequest
} }
Headers = headers; Headers = headers;
PathVariables = pathVariables; PathVariables = pathVariables;
}
public WebRequest(string version, string method, string path, WebHeaderCollection headers, Dictionary<string, string> pathVariables, string content) : this(version, method, path, headers, pathVariables)
{
Content = content;
Form = ReadOnlyDictionary<string, string>.Empty;
}
public WebRequest(string version, string method, string path, WebHeaderCollection headers, Dictionary<string, string> pathVariables, Dictionary<string, string> form) : this(version, method, path, headers, pathVariables)
{
Content = UrlAndFormEncode(form);
Form = new ReadOnlyDictionary<string, string>(form); Form = new ReadOnlyDictionary<string, string>(form);
} }
private string UrlAndFormEncode(string v)
{
v = v.UrlEncode();
return v;
}
private string UrlAndFormEncode(IEnumerable<KeyValuePair<string, string>> v)
{
StringBuilder sb = new StringBuilder();
int i = 0;
foreach (KeyValuePair<string, string> 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) private string UrlAndFormDecode(string v)
{ {
v = v.Replace('+', ' '); // must be done first to not decode '%..' => ' ' v = v.Replace('+', ' '); // must be done first to not decode '%..' => ' '

View File

@ -1,4 +1,5 @@
using System.Net; using System.Diagnostics;
using System.Net;
using System.Net.Security; using System.Net.Security;
using System.Net.Sockets; using System.Net.Sockets;
using System.Security.Authentication; using System.Security.Authentication;
@ -257,6 +258,11 @@ public class WebServer
Dictionary<string, string> form = new Dictionary<string, string>(); Dictionary<string, string> form = new Dictionary<string, string>();
string contentLength = headers["Content-Length"]; string contentLength = headers["Content-Length"];
string contentType = headers["Content-Type"];
// I never thought I'd ever actually use this!
Union<string, Dictionary<string, string>> content;
if (contentLength != "") if (contentLength != "")
{ {
int contentLengthInt = Int32.Parse(contentLength); int contentLengthInt = Int32.Parse(contentLength);
@ -265,17 +271,25 @@ public class WebServer
char[] buffer = new char[contentLengthInt]; char[] buffer = new char[contentLengthInt];
sr.ReadBlock(buffer, 0, contentLengthInt); sr.ReadBlock(buffer, 0, contentLengthInt);
string content = new string(buffer); string contentString = new string(buffer);
Console.Error.WriteLine(content); Console.Error.WriteLine(contentString);
if (!String.IsNullOrEmpty(content)) if (!String.IsNullOrEmpty(contentString))
{ {
string[] kvps = content.Split(new char[] { '&' }); if (contentType == "application/x-www-form-urlencoded")
{
string[] kvps = contentString.Split(new char[] { '&' });
foreach (string kvp in kvps) foreach (string kvp in kvps)
{ {
string[] pv = kvp.Split(new char[] { '=' }); string[] pv = kvp.Split(new char[] { '=' });
form[pv[0].UrlDecode()] = pv[1].UrlDecode().Replace('+', ' '); form[pv[0].UrlDecode()] = pv[1].UrlDecode().Replace('+', ' ');
} }
content = form;
}
else
{
content = contentString;
}
} }
} }
@ -379,6 +393,35 @@ public class WebServer
{ {
// Console.WriteLine("Client connected"); // Console.WriteLine("Client connected");
if (Debugger.IsAttached)
{
// 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);
}
*/
}
else
{
// also catch generic exceptions if we are not debugging
try try
{ {
HandleClient(client); HandleClient(client);
@ -399,7 +442,7 @@ public class WebServer
Console.Error.WriteLine(ex.Message); Console.Error.WriteLine(ex.Message);
Console.Error.WriteLine(ex.StackTrace); Console.Error.WriteLine(ex.StackTrace);
} }
}
client.Close(); client.Close();
} }
} }