From 21fa2c5af0d4caf38b7acfed97c4e5e94038efa7 Mon Sep 17 00:00:00 2001 From: Michael Becker Date: Sat, 20 Jul 2024 23:44:59 -0400 Subject: [PATCH] Initial commit --- src/lib/MBS.Web/ClientConnectedEventArgs.cs | 40 +++ src/lib/MBS.Web/ClientConnectingEventArgs.cs | 33 +++ src/lib/MBS.Web/Control.cs | 100 +++++++ src/lib/MBS.Web/CssClassList.cs | 48 ++++ .../MBS.Web/Handlers/RedirectWebHandler.cs | 24 ++ src/lib/MBS.Web/IWebHandler.cs | 6 + src/lib/MBS.Web/MBS.Web.csproj | 10 + src/lib/MBS.Web/RenderEventArgs.cs | 15 + .../UI/HtmlControls/HtmlGenericControl.cs | 46 +++ src/lib/MBS.Web/UI/HtmlControls/HtmlLink.cs | 28 ++ src/lib/MBS.Web/UI/WebControl.cs | 28 ++ src/lib/MBS.Web/UI/WebControlWithMnemonic.cs | 56 ++++ src/lib/MBS.Web/UI/WebControls/Button.cs | 41 +++ src/lib/MBS.Web/UI/WebControls/Container.cs | 22 ++ .../MBS.Web/UI/WebControls/ContainerBase.cs | 28 ++ src/lib/MBS.Web/UI/WebControls/FormView.cs | 72 +++++ src/lib/MBS.Web/UI/WebControls/Label.cs | 14 + src/lib/MBS.Web/UI/WebControls/Panel.cs | 42 +++ src/lib/MBS.Web/UI/WebControls/TextBox.cs | 40 +++ src/lib/MBS.Web/UI/WebPage.cs | 72 +++++ src/lib/MBS.Web/UI/WebStyleSheet.cs | 33 +++ src/lib/MBS.Web/WebApplication.cs | 55 ++++ src/lib/MBS.Web/WebContext.cs | 15 + src/lib/MBS.Web/WebCookie.cs | 92 ++++++ src/lib/MBS.Web/WebCookieSameSite.cs | 8 + src/lib/MBS.Web/WebCookieScope.cs | 28 ++ src/lib/MBS.Web/WebCookieSecurity.cs | 9 + src/lib/MBS.Web/WebHandler.cs | 27 ++ src/lib/MBS.Web/WebHeaderCollection.cs | 85 ++++++ src/lib/MBS.Web/WebRequest.cs | 28 ++ src/lib/MBS.Web/WebResponse.cs | 23 ++ src/lib/MBS.Web/WebRoute.cs | 142 ++++++++++ src/lib/MBS.Web/WebServer.cs | 261 ++++++++++++++++++ src/lib/MBS.Web/WebServerCreatedEventArgs.cs | 16 ++ .../WebServerProcessRequestEventArgs.cs | 18 ++ src/lib/MBS.Web/WebSession.cs | 7 + 36 files changed, 1612 insertions(+) create mode 100644 src/lib/MBS.Web/ClientConnectedEventArgs.cs create mode 100644 src/lib/MBS.Web/ClientConnectingEventArgs.cs create mode 100644 src/lib/MBS.Web/Control.cs create mode 100644 src/lib/MBS.Web/CssClassList.cs create mode 100644 src/lib/MBS.Web/Handlers/RedirectWebHandler.cs create mode 100644 src/lib/MBS.Web/IWebHandler.cs create mode 100644 src/lib/MBS.Web/MBS.Web.csproj create mode 100644 src/lib/MBS.Web/RenderEventArgs.cs create mode 100644 src/lib/MBS.Web/UI/HtmlControls/HtmlGenericControl.cs create mode 100644 src/lib/MBS.Web/UI/HtmlControls/HtmlLink.cs create mode 100644 src/lib/MBS.Web/UI/WebControl.cs create mode 100644 src/lib/MBS.Web/UI/WebControlWithMnemonic.cs create mode 100644 src/lib/MBS.Web/UI/WebControls/Button.cs create mode 100644 src/lib/MBS.Web/UI/WebControls/Container.cs create mode 100644 src/lib/MBS.Web/UI/WebControls/ContainerBase.cs create mode 100644 src/lib/MBS.Web/UI/WebControls/FormView.cs create mode 100644 src/lib/MBS.Web/UI/WebControls/Label.cs create mode 100644 src/lib/MBS.Web/UI/WebControls/Panel.cs create mode 100644 src/lib/MBS.Web/UI/WebControls/TextBox.cs create mode 100644 src/lib/MBS.Web/UI/WebPage.cs create mode 100644 src/lib/MBS.Web/UI/WebStyleSheet.cs create mode 100644 src/lib/MBS.Web/WebApplication.cs create mode 100644 src/lib/MBS.Web/WebContext.cs create mode 100644 src/lib/MBS.Web/WebCookie.cs create mode 100644 src/lib/MBS.Web/WebCookieSameSite.cs create mode 100644 src/lib/MBS.Web/WebCookieScope.cs create mode 100644 src/lib/MBS.Web/WebCookieSecurity.cs create mode 100644 src/lib/MBS.Web/WebHandler.cs create mode 100644 src/lib/MBS.Web/WebHeaderCollection.cs create mode 100644 src/lib/MBS.Web/WebRequest.cs create mode 100644 src/lib/MBS.Web/WebResponse.cs create mode 100644 src/lib/MBS.Web/WebRoute.cs create mode 100644 src/lib/MBS.Web/WebServer.cs create mode 100644 src/lib/MBS.Web/WebServerCreatedEventArgs.cs create mode 100644 src/lib/MBS.Web/WebServerProcessRequestEventArgs.cs create mode 100644 src/lib/MBS.Web/WebSession.cs diff --git a/src/lib/MBS.Web/ClientConnectedEventArgs.cs b/src/lib/MBS.Web/ClientConnectedEventArgs.cs new file mode 100644 index 0000000..58aa3fb --- /dev/null +++ b/src/lib/MBS.Web/ClientConnectedEventArgs.cs @@ -0,0 +1,40 @@ +// +// ClientConnectedEventArgs.cs +// +// Author: +// Michael Becker +// +// Copyright (c) 2021 Mike Becker's Software +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +using System; +using System.Collections.Generic; + +namespace MBS.Web; + +public class ClientConnectedEventArgs : EventArgs +{ + public bool Handled { get; set; } = false; + + public WebRequest Request { get; } = null; + public WebResponse Response { get; } = null; + + public System.Net.Sockets.TcpClient Client { get; } = null; + public ClientConnectedEventArgs(System.Net.Sockets.TcpClient client, WebRequest request, WebResponse response) + { + Client = client; + Request = request; + Response = response; + } +} diff --git a/src/lib/MBS.Web/ClientConnectingEventArgs.cs b/src/lib/MBS.Web/ClientConnectingEventArgs.cs new file mode 100644 index 0000000..34c655f --- /dev/null +++ b/src/lib/MBS.Web/ClientConnectingEventArgs.cs @@ -0,0 +1,33 @@ +// +// ClientConnectedEventArgs.cs +// +// Author: +// Michael Becker +// +// Copyright (c) 2021 Mike Becker's Software +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +using System; +using System.ComponentModel; + +namespace MBS.Web; + +public class ClientConnectingEventArgs : CancelEventArgs +{ + public System.Net.Sockets.TcpClient Client { get; } = null; + public ClientConnectingEventArgs(System.Net.Sockets.TcpClient client) + { + Client = client; + } +} diff --git a/src/lib/MBS.Web/Control.cs b/src/lib/MBS.Web/Control.cs new file mode 100644 index 0000000..13b03af --- /dev/null +++ b/src/lib/MBS.Web/Control.cs @@ -0,0 +1,100 @@ +using System.Xml; +using MBS.Web.UI; + +namespace MBS.Web; + +public abstract class Control : IWebHandler +{ + + public class ControlCollection + : System.Collections.ObjectModel.Collection + { + + } + + public Dictionary PathVariables { get; } = new Dictionary(); + public Control(Dictionary? pathVariables = null) + { + if (pathVariables == null) + pathVariables = new Dictionary(); + + PathVariables = pathVariables; + } + + protected virtual void OnInit(RenderEventArgs e) + { + } + protected virtual void RenderContents(XmlWriter writer) + { + } + + protected virtual string TagName { get; } = "div"; + public Dictionary Attributes { get; } = new Dictionary(); + public CssClassList ClassList { get; } = new CssClassList(); + + private bool _initted = false; + protected void EnsureInitialized() + { + if (_initted) return; + InitializeInternal(); + _initted = true; + } + + protected virtual void InitializeInternal() + { + } + + protected virtual void RenderBeginTag(XmlWriter writer) + { + EnsureInitialized(); + writer.WriteStartElement(TagName); + + bool classListWritten = false; + IDictionary attrs = GetControlAttributes(); + foreach (KeyValuePair kvp in attrs) + { + string value = kvp.Value; + if (kvp.Key == "class") + { + IEnumerable classList = ClassList.Union(kvp.Value.Split(new char[] { ' ' })); + value = String.Join(' ', classList); + classListWritten = true; + } + writer.WriteAttributeString(kvp.Key, value); + } + + if (!classListWritten && ClassList.Count > 0) + { + writer.WriteAttributeString("class", String.Join(' ', ClassList)); + } + } + protected virtual void RenderEndTag(XmlWriter writer) + { + writer.WriteEndElement(); + } + + public void Render(XmlWriter writer) + { + RenderBeginTag(writer); + RenderContents(writer); + RenderEndTag(writer); + } + + public void ProcessRequest(WebContext context) + { + context.Response.ContentType = "application/xhtml+xml"; + + OnInit(new RenderEventArgs(context.Request, context.Response)); + + XmlWriter writer = XmlWriter.Create(context.Response.Stream); + Render(writer); + + writer.Flush(); + writer.Close(); + } + + protected virtual IDictionary GetControlAttributes() + { + return Attributes; + } +} diff --git a/src/lib/MBS.Web/CssClassList.cs b/src/lib/MBS.Web/CssClassList.cs new file mode 100644 index 0000000..dd8bde4 --- /dev/null +++ b/src/lib/MBS.Web/CssClassList.cs @@ -0,0 +1,48 @@ +namespace MBS.Web; + +public class CssClassList : List +{ + // + // Summary: + // Initializes a new instance of the System.Collections.Generic.List`1 class that + // is empty and has the default initial capacity. + public CssClassList() : base() { } + // + // Summary: + // Initializes a new instance of the System.Collections.Generic.List`1 class that + // contains elements copied from the specified collection and has sufficient capacity + // to accommodate the number of elements copied. + // + // Parameters: + // collection: + // The collection whose elements are copied to the new list. + // + // Exceptions: + // T:System.ArgumentNullException: + // collection is null. + public CssClassList(IEnumerable collection) : base(collection) { } + // + // Summary: + // Initializes a new instance of the System.Collections.Generic.List`1 class that + // is empty and has the specified initial capacity. + // + // Parameters: + // capacity: + // The number of elements that the new list can initially store. + // + // Exceptions: + // T:System.ArgumentOutOfRangeException: + // capacity is less than 0. + public CssClassList(int capacity) : base(capacity) { } + + public new void Add(string value) + { + string[] vals = value.Split(new char[] { ' ' }); + foreach (string val in vals) + { + string v = val.Trim(); + if (!String.IsNullOrEmpty(v)) + base.Add(val.Trim()); + } + } +} \ No newline at end of file diff --git a/src/lib/MBS.Web/Handlers/RedirectWebHandler.cs b/src/lib/MBS.Web/Handlers/RedirectWebHandler.cs new file mode 100644 index 0000000..a5ffe97 --- /dev/null +++ b/src/lib/MBS.Web/Handlers/RedirectWebHandler.cs @@ -0,0 +1,24 @@ + +namespace MBS.Web; + +public class RedirectWebHandler : IWebHandler +{ + public string TargetUrl { get; } + public RedirectWebHandler(string targetUrl) + { + TargetUrl = targetUrl; + } + + public void ProcessRequest(WebContext context) + { + context.Response.ResponseCode = 302; + context.Response.ResponseText = "Found"; + + string actualUrl = TargetUrl.Replace("~/", context.Application.VirtualBasePath); + foreach (KeyValuePair kvp in context.Request.PathVariables) + { + actualUrl = actualUrl.Replace("{" + kvp.Key + "}", kvp.Value); + } + context.Response.Headers.Add("Location", actualUrl); + } +} \ No newline at end of file diff --git a/src/lib/MBS.Web/IWebHandler.cs b/src/lib/MBS.Web/IWebHandler.cs new file mode 100644 index 0000000..01e00ee --- /dev/null +++ b/src/lib/MBS.Web/IWebHandler.cs @@ -0,0 +1,6 @@ +namespace MBS.Web; + +public interface IWebHandler +{ + void ProcessRequest(WebContext context); +} diff --git a/src/lib/MBS.Web/MBS.Web.csproj b/src/lib/MBS.Web/MBS.Web.csproj new file mode 100644 index 0000000..192a048 --- /dev/null +++ b/src/lib/MBS.Web/MBS.Web.csproj @@ -0,0 +1,10 @@ + + + + + + net8.0 + enable + enable + + \ No newline at end of file diff --git a/src/lib/MBS.Web/RenderEventArgs.cs b/src/lib/MBS.Web/RenderEventArgs.cs new file mode 100644 index 0000000..7a77606 --- /dev/null +++ b/src/lib/MBS.Web/RenderEventArgs.cs @@ -0,0 +1,15 @@ +namespace MBS.Web; + +public class RenderEventArgs : EventArgs +{ + + public WebRequest Request { get; } + public WebResponse Response { get; } + + public RenderEventArgs(WebRequest request, WebResponse response) + { + Request = request; + Response = response; + } + +} \ No newline at end of file diff --git a/src/lib/MBS.Web/UI/HtmlControls/HtmlGenericControl.cs b/src/lib/MBS.Web/UI/HtmlControls/HtmlGenericControl.cs new file mode 100644 index 0000000..c4853c1 --- /dev/null +++ b/src/lib/MBS.Web/UI/HtmlControls/HtmlGenericControl.cs @@ -0,0 +1,46 @@ +namespace MBS.Web.UI.HtmlControls; + +using System.Xml; +using MBS.Core.Collections.Generic; + +public class HtmlGenericControl : Control +{ + private string _tagName; + protected override string TagName => _tagName; + + public string? Content { get; set; } = null; + + public HtmlGenericControl(string tagName) + { + _tagName = tagName; + } + public HtmlGenericControl(string tagName, IEnumerable> attributes, IList classes, string? content = null) + { + _tagName = tagName; + Attributes.AddRange(attributes); + ClassList.AddRange(classes); + Content = content; + } + public HtmlGenericControl(string tagName, IEnumerable> attributes, string? content = null) + { + _tagName = tagName; + Attributes.AddRange(attributes); + Content = content; + } + public HtmlGenericControl(string tagName, IEnumerable classes, string? content = null) + { + _tagName = tagName; + ClassList.AddRange(classes); + Content = content; + } + + protected override void RenderContents(XmlWriter writer) + { + base.RenderContents(writer); + if (Content != null) + { + writer.WriteRaw(Content); + } + } + +} \ No newline at end of file diff --git a/src/lib/MBS.Web/UI/HtmlControls/HtmlLink.cs b/src/lib/MBS.Web/UI/HtmlControls/HtmlLink.cs new file mode 100644 index 0000000..d532d71 --- /dev/null +++ b/src/lib/MBS.Web/UI/HtmlControls/HtmlLink.cs @@ -0,0 +1,28 @@ + +namespace MBS.Web.UI.HtmlControls; + +public class HtmlLink : Control +{ + + public string Rel { get; set; } + public string ContentType { get; set; } + public string TargetUrl { get; set; } + + public HtmlLink(string rel, string contentType, string targetUrl) + { + Rel = rel; + ContentType = contentType; + TargetUrl = targetUrl; + } + + protected override IDictionary GetControlAttributes() + { + IDictionary list = base.GetControlAttributes(); + list.Add("rel", Rel); + list.Add("type", ContentType); + list.Add("href", TargetUrl); + return list; + } + protected override string TagName => "link"; + +} \ No newline at end of file diff --git a/src/lib/MBS.Web/UI/WebControl.cs b/src/lib/MBS.Web/UI/WebControl.cs new file mode 100644 index 0000000..bb80c9a --- /dev/null +++ b/src/lib/MBS.Web/UI/WebControl.cs @@ -0,0 +1,28 @@ +using System.Xml; +using MBS.Core; + +namespace MBS.Web.UI; + +public abstract class WebControl : Control +{ + private NanoId _NanoId = NanoId.Empty; + private string NanoIdString + { + get + { + if (_NanoId.IsEmpty) + { + _NanoId = NanoId.Generate(NanoId.CapitalAlphanumericNoSpecialChars, 8); + } + return _NanoId.ToString(); + } + } + public string ClientId { get { return String.Format("UWT{0}", NanoIdString); } } + + protected override IDictionary GetControlAttributes() + { + IDictionary dict = base.GetControlAttributes(); + dict["id"] = ClientId; + return dict; + } +} \ No newline at end of file diff --git a/src/lib/MBS.Web/UI/WebControlWithMnemonic.cs b/src/lib/MBS.Web/UI/WebControlWithMnemonic.cs new file mode 100644 index 0000000..f46e6b9 --- /dev/null +++ b/src/lib/MBS.Web/UI/WebControlWithMnemonic.cs @@ -0,0 +1,56 @@ +using System.Xml; + +namespace MBS.Web.UI; + +public abstract class WebControlWithMnemonic : WebControl +{ + public WebControlWithMnemonic(string text) + { + Text = text; + } + + public string Text { get; set; } + public bool UseMnemonic { get; set; } = true; + public bool HasMnemonic { get { return MnemonicChar != '\0'; } } + public char MnemonicChar + { + get + { + string[] textForMnemonic = Text.Split(new char[] { '_' }, 2); + if (textForMnemonic.Length > 1) + { + return textForMnemonic[1][0]; + } + return '\0'; + } + } + + + protected string GetTextWithoutMnemonic() + { + if (UseMnemonic) + { + return Text.Replace("_", String.Empty); + } + return Text; + } + protected void WriteTextWithMnemonic(XmlWriter writer) + { + if (UseMnemonic) + { + string[] textForMnemonic = Text.Split(new char[] { '_' }, 2); + if (textForMnemonic.Length > 1) + { + // Blah blah the n_ext underline + writer.WriteRaw(textForMnemonic[0]); + writer.WriteStartElement("u"); + writer.WriteRaw(textForMnemonic[1].Substring(0, 1)); + writer.WriteEndElement(); + writer.WriteRaw(textForMnemonic[1].Substring(1)); + return; + } + } + writer.WriteRaw(Text); + } + +} diff --git a/src/lib/MBS.Web/UI/WebControls/Button.cs b/src/lib/MBS.Web/UI/WebControls/Button.cs new file mode 100644 index 0000000..6bb0441 --- /dev/null +++ b/src/lib/MBS.Web/UI/WebControls/Button.cs @@ -0,0 +1,41 @@ + +using System.Xml; + +namespace MBS.Web.UI.WebControls; + +public class Button : WebControlWithMnemonic +{ + public bool UseSubmitBehavior { get; set; } = false; + + protected override string TagName => "button"; // UseSubmitBehavior ? "input" : "button"; + + public Button(string text) : base(text) { } + + protected override void RenderContents(XmlWriter writer) + { + // if (!UseSubmitBehavior) + { + WriteTextWithMnemonic(writer); + } + } + + protected override IDictionary GetControlAttributes() + { + IDictionary attrs = base.GetControlAttributes(); + if (UseSubmitBehavior) + { + attrs["type"] = "submit"; + // attrs["value"] = GetTextWithoutMnemonic(); + } + // else + { + if (HasMnemonic) + { + attrs["accesskey"] = MnemonicChar.ToString(); + } + } + return attrs; + } + + +} diff --git a/src/lib/MBS.Web/UI/WebControls/Container.cs b/src/lib/MBS.Web/UI/WebControls/Container.cs new file mode 100644 index 0000000..0610385 --- /dev/null +++ b/src/lib/MBS.Web/UI/WebControls/Container.cs @@ -0,0 +1,22 @@ +namespace MBS.Web.UI.WebControls; + +public class Container : ContainerBase +{ + protected override string TagName => _tagName; + + private string _tagName = "div"; + public Container() + { + } + public Container(string tagName) + { + _tagName = tagName; + } + + public Control.ControlCollection Controls { get; } = new Control.ControlCollection(); + + protected override IList GetChildControls() + { + return Controls; + } +} \ No newline at end of file diff --git a/src/lib/MBS.Web/UI/WebControls/ContainerBase.cs b/src/lib/MBS.Web/UI/WebControls/ContainerBase.cs new file mode 100644 index 0000000..4488220 --- /dev/null +++ b/src/lib/MBS.Web/UI/WebControls/ContainerBase.cs @@ -0,0 +1,28 @@ +using System.Xml; + +namespace MBS.Web.UI.WebControls; + +public abstract class ContainerBase : WebControl +{ + private List _list = new List(); + protected virtual IList GetChildControls() + { + return _list; + } + + private IList? _childControls = null; + + protected override void RenderContents(XmlWriter writer) + { + base.RenderContents(writer); + + if (_childControls == null) + { + _childControls = GetChildControls(); + } + foreach (Control control in _childControls) + { + control.Render(writer); + } + } +} \ No newline at end of file diff --git a/src/lib/MBS.Web/UI/WebControls/FormView.cs b/src/lib/MBS.Web/UI/WebControls/FormView.cs new file mode 100644 index 0000000..cf342f4 --- /dev/null +++ b/src/lib/MBS.Web/UI/WebControls/FormView.cs @@ -0,0 +1,72 @@ +using MBS.Core.Collections.Generic; +using MBS.Web.UI.HtmlControls; + +namespace MBS.Web.UI.WebControls; + +public class FormView : ContainerBase +{ + public class FormViewItem : WebControl + { + public class FormViewItemCollection + : System.Collections.ObjectModel.Collection + { + + } + + public string Title { get; set; } + public WebControl Control { get; set; } + + public FormViewItem(string title, WebControl control) + { + Title = title; + Control = control; + } + } + + public FormViewItem.FormViewItemCollection Items { get; } = new FormViewItem.FormViewItemCollection(); + + protected override void InitializeInternal() + { + base.InitializeInternal(); + ClassList.Add("uwt-formview"); + } + + protected override string TagName => "table"; + + protected override IList GetChildControls() + { + List list = new List(); + foreach (FormViewItem item in Items) + { + Container tr = new Container("tr"); + tr.ClassList.Add("uwt-formview-item"); + + Label lbl = new Label(item.Title); + Container tdLabel = new Container("td"); + tdLabel.ClassList.Add("uwt-formview-item-label"); + tdLabel.Controls.Add(lbl); + tr.Controls.Add(tdLabel); + + Container tdContent = new Container("td"); + tdContent.ClassList.Add("uwt-formview-item-content"); + if (lbl.HasMnemonic) + { + item.Control.Attributes.Add("accesskey", lbl.MnemonicChar.ToString()); + } + tdContent.Controls.Add(item.Control); + tr.Controls.Add(tdContent); + + list.Add(tr); + } + return list; + } + + public FormView(IEnumerable? items = null) + { + if (items != null) + { + Items.AddRange(items); + } + } + +} diff --git a/src/lib/MBS.Web/UI/WebControls/Label.cs b/src/lib/MBS.Web/UI/WebControls/Label.cs new file mode 100644 index 0000000..7427cdd --- /dev/null +++ b/src/lib/MBS.Web/UI/WebControls/Label.cs @@ -0,0 +1,14 @@ +using System.Xml; + +namespace MBS.Web.UI.WebControls; + +public class Label : WebControlWithMnemonic +{ + protected override string TagName => "label"; + + public Label(string text) : base(text) { } + protected override void RenderContents(XmlWriter writer) + { + WriteTextWithMnemonic(writer); + } +} \ No newline at end of file diff --git a/src/lib/MBS.Web/UI/WebControls/Panel.cs b/src/lib/MBS.Web/UI/WebControls/Panel.cs new file mode 100644 index 0000000..c0abb25 --- /dev/null +++ b/src/lib/MBS.Web/UI/WebControls/Panel.cs @@ -0,0 +1,42 @@ + + +using MBS.Core.Collections.Generic; + +namespace MBS.Web.UI.WebControls; + +public class Panel : ContainerBase +{ + protected override void InitializeInternal() + { + base.InitializeInternal(); + ClassList.Add("uwt-panel"); + } + + public List HeaderControls { get; } = new List(); + public List ContentControls { get; } = new List(); + public List FooterControls { get; } = new List(); + + protected override string TagName => "div"; + protected override IList GetChildControls() + { + List actual = new List(); + + Container divHeader = new Container(); + divHeader.ClassList.Add("uwt-header"); + divHeader.Controls.AddRange(HeaderControls); + actual.Add(divHeader); + + Container divContent = new Container(); + divContent.ClassList.Add("uwt-content"); + divContent.Controls.AddRange(ContentControls); + actual.Add(divContent); + + Container divFooter = new Container(); + divFooter.ClassList.Add("uwt-footer"); + divFooter.Controls.AddRange(FooterControls); + actual.Add(divFooter); + + return actual; + } + +} \ No newline at end of file diff --git a/src/lib/MBS.Web/UI/WebControls/TextBox.cs b/src/lib/MBS.Web/UI/WebControls/TextBox.cs new file mode 100644 index 0000000..fba2dd5 --- /dev/null +++ b/src/lib/MBS.Web/UI/WebControls/TextBox.cs @@ -0,0 +1,40 @@ + +namespace MBS.Web.UI.WebControls; + +public enum TextBoxType +{ + None, + Password +} + +public class TextBox : WebControl +{ + public TextBoxType TextBoxType { get; set; } = TextBoxType.None; + + public TextBox(TextBoxType type = TextBoxType.None) + { + TextBoxType = type; + } + + protected override string TagName => "input"; + + public string Name { get; set; } + + private string GetTextBoxType() + { + switch (TextBoxType) + { + case TextBoxType.Password: + return "password"; + } + return "text"; + } + + protected override IDictionary GetControlAttributes() + { + IDictionary dict = base.GetControlAttributes(); + dict["type"] = GetTextBoxType(); + dict["name"] = Name; + return dict; + } +} diff --git a/src/lib/MBS.Web/UI/WebPage.cs b/src/lib/MBS.Web/UI/WebPage.cs new file mode 100644 index 0000000..2c77f4e --- /dev/null +++ b/src/lib/MBS.Web/UI/WebPage.cs @@ -0,0 +1,72 @@ +using System.Xml; +using MBS.Web.UI.HtmlControls; + +namespace MBS.Web.UI; + +public class WebPage : Control +{ + public WebPage(Dictionary? pathVariables = null) : base(pathVariables) { } + + public Control.ControlCollection HeaderControls { get; } = new Control.ControlCollection(); + public Control.ControlCollection Controls { get; } = new Control.ControlCollection(); + + public WebStyleSheet.WebStyleSheetCollection StyleSheets { get; } = new WebStyleSheet.WebStyleSheetCollection(); + + private bool ChildControlsCreated { get; set; } = false; + protected virtual void CreateChildControls() + { + } + + protected override string TagName => "html"; + protected override void RenderBeginTag(XmlWriter writer) + { + EnsureInitialized(); + writer.WriteStartElement(TagName, "http://www.w3.org/1999/xhtml"); + } + + protected override void RenderContents(XmlWriter writer) + { + if (!ChildControlsCreated) + { + CreateChildControls(); + ChildControlsCreated = true; + } + + writer.WriteStartElement("head"); + writer.WriteElementString("title", "Mocha Application"); + + List ctls = new List(); + ctls.Add(new HtmlLink("stylesheet", "text/css", "/madi/asset/ui-html/2024.27.5/css/mochaApp.css?plate=BMT216A&sha256-XjJJ2%2BcFxZXtxY579nwOKBNYdP1KUySxNDbxR4QGxvQ%3D")); + + foreach (WebStyleSheet ss in StyleSheets) + { + if (ss.FileName != null) + { + ctls.Add(new HtmlLink("stylesheet", ss.ContentType, ss.FileName)); + } + else if (ss.Content != null) + { + ctls.Add(new HtmlGenericControl("style", new KeyValuePair[] { new KeyValuePair("type", ss.ContentType) }, ss.Content)); + } + } + + foreach (Control control in ctls) + { + control.Render(writer); + } + + writer.WriteEndElement(); + + writer.WriteStartElement("body"); + + writer.WriteStartElement("form"); + writer.WriteAttributeString("method", "POST"); + foreach (WebControl control in Controls) + { + control.Render(writer); + } + writer.WriteEndElement(); + + writer.WriteEndElement(); + } +} diff --git a/src/lib/MBS.Web/UI/WebStyleSheet.cs b/src/lib/MBS.Web/UI/WebStyleSheet.cs new file mode 100644 index 0000000..bffa51e --- /dev/null +++ b/src/lib/MBS.Web/UI/WebStyleSheet.cs @@ -0,0 +1,33 @@ +namespace MBS.Web.UI; + +public class WebStyleSheet +{ + public class WebStyleSheetCollection + : System.Collections.ObjectModel.Collection + { + + } + + public string ContentType { get; set; } + public string? Content { get; set; } + public string? FileName { get; set; } + + private WebStyleSheet(string contentType) + { + ContentType = contentType; + } + + public static WebStyleSheet FromContent(string contentType, string content) + { + WebStyleSheet styleSheet = new WebStyleSheet(contentType); + styleSheet.Content = content; + return styleSheet; + } + + public static WebStyleSheet FromFile(string contentType, string filename) + { + WebStyleSheet styleSheet = new WebStyleSheet(contentType); + styleSheet.FileName = filename; + return styleSheet; + } +} \ No newline at end of file diff --git a/src/lib/MBS.Web/WebApplication.cs b/src/lib/MBS.Web/WebApplication.cs new file mode 100644 index 0000000..55be34c --- /dev/null +++ b/src/lib/MBS.Web/WebApplication.cs @@ -0,0 +1,55 @@ +using System.ComponentModel; +using System.Net; +using MBS.Core; +using MBS.Web.UI; + +namespace MBS.Web; + +public abstract class WebApplication : Application +{ + protected abstract int DefaultPort { get; } + + public string VirtualBasePath { get; set; } = "/"; + + protected override void OnBeforeStartInternal(CancelEventArgs e) + { + base.OnBeforeStartInternal(e); + + CommandLine.Options.Add("port", 'p', 10200, CommandLineOptionValueType.Single, "the port on which to listen for HTTP(S) requests"); + } + + public EventHandler ProcessRequest; + protected virtual void OnProcessRequest(WebServerProcessRequestEventArgs e) + { + ProcessRequest?.Invoke(this, e); + } + + private void server_OnProcessRequest(object sender, WebServerProcessRequestEventArgs e) + { + OnProcessRequest(e); + } + + public event EventHandler ServerCreated; + protected virtual void OnServerCreated(WebServerCreatedEventArgs e) + { + ServerCreated?.Invoke(this, e); + } + + protected override void OnStartup(EventArgs e) + { + base.OnStartup(e); + + Console.WriteLine("starting server..."); + + WebServer server = new WebServer(new IPEndPoint(IPAddress.Any, DefaultPort)); + OnServerCreated(new WebServerCreatedEventArgs(server)); + server.ProcessRequest += server_OnProcessRequest; + server.Start(); + + while (true) + { + Thread.Sleep(50); + } + } + +} diff --git a/src/lib/MBS.Web/WebContext.cs b/src/lib/MBS.Web/WebContext.cs new file mode 100644 index 0000000..b88742e --- /dev/null +++ b/src/lib/MBS.Web/WebContext.cs @@ -0,0 +1,15 @@ +namespace MBS.Web; + +public class WebContext +{ + public WebContext(WebApplication application, WebRequest request, WebResponse response) + { + Application = application; + Request = request; + Response = response; + } + + public WebApplication Application { get; } + public WebRequest Request { get; } + public WebResponse Response { get; } +} diff --git a/src/lib/MBS.Web/WebCookie.cs b/src/lib/MBS.Web/WebCookie.cs new file mode 100644 index 0000000..9b9c3f1 --- /dev/null +++ b/src/lib/MBS.Web/WebCookie.cs @@ -0,0 +1,92 @@ +using System.Text; + +namespace MBS.Web; + +public class WebCookie +{ + public class WebCookieCollection + : System.Collections.ObjectModel.Collection + { + private Dictionary _list = new Dictionary(); + + public void Add(string key, string value, WebCookieScope scope, WebCookieSecurity security, WebCookieSameSite sameSite) + { + WebCookie cookie = new WebCookie(); + cookie.key = key; + cookie.value = value; + cookie.scope = scope; + cookie.security = security; + cookie.sameSite = sameSite; + _list[key] = cookie; + base.Add(cookie); + } + public string this[string key] + { + get + { + if (_list.ContainsKey(key)) + { + return _list[key].value; + } + return null; + } + } + } + + + public string key; + public string value; + public WebCookieScope scope; + public WebCookieSecurity security; + public WebCookieSameSite sameSite; + + + public string GetCookieString() + { + StringBuilder sb = new StringBuilder(); + sb.Append(key); + sb.Append("="); + sb.Append(value); + if (!scope.IsEmpty) + { + sb.Append("; "); + if (scope.path != null) + { + sb.Append("Path="); + sb.Append(scope.path); + } + } + if (security != WebCookieSecurity.None) + { + sb.Append("; "); + List parms = new List(); + if ((security & WebCookieSecurity.Secure) == WebCookieSecurity.Secure) + { + parms.Add("Secure"); + } + if ((security & WebCookieSecurity.HttpOnly) == WebCookieSecurity.HttpOnly) + { + parms.Add("HttpOnly"); + } + sb.Append(String.Join("; ", parms)); + } + if (sameSite != WebCookieSameSite.Lax) + { + sb.Append("; SameSite="); + switch (sameSite) + { + case WebCookieSameSite.None: + { + sb.Append("None"); + break; + } + case WebCookieSameSite.Strict: + { + sb.Append("Strict"); + break; + } + } + } + return sb.ToString(); + } +} \ No newline at end of file diff --git a/src/lib/MBS.Web/WebCookieSameSite.cs b/src/lib/MBS.Web/WebCookieSameSite.cs new file mode 100644 index 0000000..dde671b --- /dev/null +++ b/src/lib/MBS.Web/WebCookieSameSite.cs @@ -0,0 +1,8 @@ +namespace MBS.Web; + +public enum WebCookieSameSite +{ + None = 0, + Strict = 1, + Lax = 2 +} diff --git a/src/lib/MBS.Web/WebCookieScope.cs b/src/lib/MBS.Web/WebCookieScope.cs new file mode 100644 index 0000000..0021e99 --- /dev/null +++ b/src/lib/MBS.Web/WebCookieScope.cs @@ -0,0 +1,28 @@ +namespace MBS.Web; + +public struct WebCookieScope +{ + public string? domain; + public string? path; + private bool _isNotEmpty; + public bool IsEmpty { get { return !_isNotEmpty; } } + + public static WebCookieScope FromDomain(string domain) + { + WebCookieScope scope = new WebCookieScope(); + scope.domain = domain; + scope.path = null; + scope._isNotEmpty = true; + return scope; + } + public static WebCookieScope FromPath(string path) + { + WebCookieScope scope = new WebCookieScope(); + scope.domain = null; + scope.path = path; + scope._isNotEmpty = true; + return scope; + } + + public static WebCookieScope Empty; +} diff --git a/src/lib/MBS.Web/WebCookieSecurity.cs b/src/lib/MBS.Web/WebCookieSecurity.cs new file mode 100644 index 0000000..c4274cd --- /dev/null +++ b/src/lib/MBS.Web/WebCookieSecurity.cs @@ -0,0 +1,9 @@ +namespace MBS.Web; + +[Flags()] +public enum WebCookieSecurity +{ + None = 0, + Secure = 1, + HttpOnly = 2 +} diff --git a/src/lib/MBS.Web/WebHandler.cs b/src/lib/MBS.Web/WebHandler.cs new file mode 100644 index 0000000..7d01817 --- /dev/null +++ b/src/lib/MBS.Web/WebHandler.cs @@ -0,0 +1,27 @@ +namespace MBS.Web; + +public class WebHandler : IWebHandler +{ + public Action? Action { get; } + public WebHandler() : this(null) { } + public WebHandler(Action? action) + { + Action = action; + } + + public void ProcessRequest(WebContext context) + { + ProcessRequestInternal(context); + } + + protected virtual void ProcessRequestInternal(WebContext context) + { + if (Action == null) + { + context.Response.ResponseCode = 404; + context.Response.ResponseText = "Not Found"; + return; + } + Action(context); + } +} diff --git a/src/lib/MBS.Web/WebHeaderCollection.cs b/src/lib/MBS.Web/WebHeaderCollection.cs new file mode 100644 index 0000000..2ae349f --- /dev/null +++ b/src/lib/MBS.Web/WebHeaderCollection.cs @@ -0,0 +1,85 @@ +using System.Net.Http.Headers; +using MBS.Core; + +namespace MBS.Web; + +public class WebHeaderCollection : List> +{ + private Dictionary> _list = new Dictionary>(); + public string? this[System.Net.HttpRequestHeader key] + { + get + { + switch (key) + { + case System.Net.HttpRequestHeader.Accept: return this["Accept"]; + case System.Net.HttpRequestHeader.AcceptCharset: return this["Accept-Charset"]; + case System.Net.HttpRequestHeader.AcceptEncoding: return this["Accept-Encoding"]; + case System.Net.HttpRequestHeader.AcceptLanguage: return this["Accept-Language"]; + case System.Net.HttpRequestHeader.Allow: return this["Allow"]; + case System.Net.HttpRequestHeader.Authorization: return this["Authorization"]; + case System.Net.HttpRequestHeader.CacheControl: return this["Cache-Control"]; + case System.Net.HttpRequestHeader.Connection: return this["Connection"]; + case System.Net.HttpRequestHeader.ContentEncoding: return this["Content-Encoding"]; + case System.Net.HttpRequestHeader.ContentLanguage: return this["Content-Language"]; + case System.Net.HttpRequestHeader.ContentLength: return this["Content-Length"]; + case System.Net.HttpRequestHeader.ContentLocation: return this["Content-Location"]; + case System.Net.HttpRequestHeader.ContentMd5: return this["Content-MD5"]; + case System.Net.HttpRequestHeader.ContentRange: return this["Content-Range"]; + case System.Net.HttpRequestHeader.ContentType: return this["Content-Type"]; + case System.Net.HttpRequestHeader.Cookie: return this["Cookie"]; + case System.Net.HttpRequestHeader.Date: return this["Date"]; + case System.Net.HttpRequestHeader.Expect: return this["Expect"]; + } + throw new ArgumentOutOfRangeException(); + } + } + + public string? this[string key] + { + get + { + if (_list.ContainsKey(key)) + { + if (_list[key].Count > 0) + { + return _list[key][_list[key].Count - 1]; + } + } + return null; + } + set + { + if (!_list.ContainsKey(key)) + { + _list[key] = new List(); + } + if (!_list[key].Contains(value)) + { + _list[key].Add(value); + } + } + } + + public IReadOnlyList GetValues(string key) + { + return _list[key]; + } + + public void Add(string key, string value) + { + Add(new KeyValuePair(key, value)); + } + public new void Add(KeyValuePair value) + { + if (!_list.ContainsKey(value.Key)) + { + _list[value.Key] = new List(); + } + if (!_list[value.Key].Contains(value.Value)) + { + _list[value.Key].Add(value.Value); + } + base.Add(value); + } +} diff --git a/src/lib/MBS.Web/WebRequest.cs b/src/lib/MBS.Web/WebRequest.cs new file mode 100644 index 0000000..1d04edb --- /dev/null +++ b/src/lib/MBS.Web/WebRequest.cs @@ -0,0 +1,28 @@ +using System.Net; + +namespace MBS.Web; + +public class WebRequest +{ + public string Version { get; } + public string Method { get; } + public string Path { get; } + + public Uri? Uri { get; } = null; + + public Dictionary PathVariables { get; } + public WebHeaderCollection Headers { get; } + + public WebRequest(string version, string method, string path, WebHeaderCollection headers, Dictionary pathVariables) + { + Version = version; + Method = method; + Path = path; + if (Uri.TryCreate(path, UriKind.Relative, out Uri? uri)) + { + Uri = uri; + } + Headers = headers; + PathVariables = pathVariables; + } +} diff --git a/src/lib/MBS.Web/WebResponse.cs b/src/lib/MBS.Web/WebResponse.cs new file mode 100644 index 0000000..6f3e189 --- /dev/null +++ b/src/lib/MBS.Web/WebResponse.cs @@ -0,0 +1,23 @@ + +using MBS.Core; + +namespace MBS.Web; + +public class WebResponse +{ + public int ResponseCode { get; set; } = 200; + public string ResponseText { get; set; } = "OK"; + + public WebHeaderCollection Headers { get; } = new WebHeaderCollection(); + public WebCookie.WebCookieCollection Cookies { get; } = new WebCookie.WebCookieCollection(); + + public MemoryStream Stream { get; } = new MemoryStream(); + public string? ContentType { get; set; } = null; + + public void Redirect(string path) + { + ResponseCode = 302; + ResponseText = "Found"; + Headers.Add("Location", path.Replace("~/", ((WebApplication)Application.Instance).VirtualBasePath)); + } +} diff --git a/src/lib/MBS.Web/WebRoute.cs b/src/lib/MBS.Web/WebRoute.cs new file mode 100644 index 0000000..38ae848 --- /dev/null +++ b/src/lib/MBS.Web/WebRoute.cs @@ -0,0 +1,142 @@ + +namespace MBS.Web; + +public class WebRoute +{ + public class WebRouteCollection + : System.Collections.ObjectModel.Collection + { + + } + + public string PathTemplate { get; } + public IWebHandler Handler { get; } + + public WebRoute(string pathTemplate, IWebHandler handler) + { + PathTemplate = pathTemplate; + Handler = handler; + } + + public bool Matches(string pathstr, Dictionary variables) + { + int i = 0; + int j = 0; + bool escape = false; + bool insideVariable = false; + + string? varname = null, varvalue = null; + + while (i < PathTemplate.Length) + { + if (PathTemplate[i] == '\\') + { + escape = true; + i++; + continue; + } + + if (!escape && (PathTemplate[i] == '{' || (PathTemplate[i] == '$' && PathTemplate.Length - i > 0 && PathTemplate[i + 1] == '('))) + { + insideVariable = true; + varname = ""; + varvalue = ""; + i++; + continue; + } + else if (!escape && (insideVariable && (PathTemplate[i] == '}' || PathTemplate[i] == ')'))) + { + insideVariable = false; + i++; + continue; + } + else + { + if (insideVariable) + { + varname += PathTemplate[i]; + i++; + continue; + } + else + { + if (PathTemplate[i] == pathstr[j]) + { + // yay, we match + // save the last-known variable, if any... + if (varname != null && varvalue != null) + { + variables[varname] = varvalue; + + // don't forget to reset it + varname = null; + varvalue = null; + } + + // ... and keep going + i++; + j++; + + if (i > PathTemplate.Length - 1 || j > pathstr.Length - 1) + { + if (i > PathTemplate.Length - 1 && j > pathstr.Length - 1) + { + // full match with no path vars + return true; + } + return false; + } + } + else + { + // no match + if (varname != null) + { + if (j + 1 < pathstr.Length) + { + // we are currently reading a variable value + varvalue += pathstr[j]; + j++; + } + else + { + // don't even question it, just return false since we should never reach this point + return false; + } + continue; + } + else + { + // we do not match! + return false; + } + + if (varvalue != null) + { + // we are in a variable + varvalue += pathstr[j]; + j++; + continue; + } + } + } + } + + escape = false; + } + + if (varvalue != null && j < pathstr.Length) + { + while (j < pathstr.Length) + { + varvalue += pathstr[j]; + j++; + } + + variables[varname] = varvalue; + varname = null; + varvalue = null; + } + return true; + } +} diff --git a/src/lib/MBS.Web/WebServer.cs b/src/lib/MBS.Web/WebServer.cs new file mode 100644 index 0000000..8479d6b --- /dev/null +++ b/src/lib/MBS.Web/WebServer.cs @@ -0,0 +1,261 @@ +using System.Net; +using System.Text; +using MBS.Core; +using MBS.Web.UI; + +namespace MBS.Web; + +public class WebServer +{ + /// + /// A of key/value pairs to send along with every request. + /// + /// + /// + /// + public Dictionary Headers { get; } = new Dictionary(); + + public Dictionary Sessions { get; } = new Dictionary(); + + public string? UserAgent + { + get + { + if (Headers.ContainsKey("Server")) + return Headers["Server"]; + return null; + } + set + { + if (value != null) + { + Headers["Server"] = value; + } + else if (Headers.ContainsKey("Server")) + { + Headers.Remove("Server"); + } + } + } + + public System.Net.IPEndPoint EndPoint { get; } + + public WebServer(System.Net.IPEndPoint endpoint) + { + EndPoint = endpoint; + } + + public WebRoute.WebRouteCollection Routes { get; } = new WebRoute.WebRouteCollection(); + + public event EventHandler ProcessRequest; + protected virtual void OnProcessRequest(WebServerProcessRequestEventArgs e) + { + ProcessRequest?.Invoke(this, e); + } + + private void tServer_ThreadStart() + { + System.Net.Sockets.TcpListener listener = new System.Net.Sockets.TcpListener(EndPoint); + listener.Start(); + + while (!_Stopping) + { + System.Net.Sockets.TcpClient client = listener.AcceptTcpClient(); + + Thread tClientThread = new Thread(tClientThread_ParameterizedThreadStart); + tClientThread.Start(client); + } + } + // JSESSIONID=0068F5AD24A89AB4AFCC4057F619EADF.authgwy-prod-mzzygbiy.prod-ui-auth.pr502.cust.pdx.wd; Path=/; Secure; HttpOnly; SameSite=None + + private void WriteResponse(WebContext context, Stream stream) + { + StreamWriter sw = new StreamWriter(stream); + sw.WriteLine(String.Format("HTTP/1.1 {0} {1}", context.Response.ResponseCode, context.Response.ResponseText)); + + if (context.Response.ContentType != null) + { + sw.WriteLine(String.Format("Content-Type: {0}", context.Response.ContentType)); + } + else if (context.Response.Headers[HttpRequestHeader.ContentType] != null) + { + sw.WriteLine(String.Format("Content-Type: {0}", context.Response.Headers[HttpRequestHeader.ContentType])); + } + + foreach (KeyValuePair header in Headers) + { + sw.WriteLine(String.Format("{0}: {1}", header.Key, header.Value)); + } + foreach (KeyValuePair header in context.Response.Headers) + { + sw.WriteLine(String.Format("{0}: {1}", header.Key, header.Value)); + } + foreach (WebCookie cookie in context.Response.Cookies) + { + sw.WriteLine(String.Format("Set-Cookie: {0}", cookie.GetCookieString())); + } + sw.WriteLine(); + sw.Flush(); + + byte[] data = context.Response.Stream.ToArray(); + Console.WriteLine("response data stream length: {0} bytes", data.Length); + + stream.Write(data, 0, data.Length); + // Console.Write(System.Text.Encoding.UTF8.GetString(data)); + stream.Flush(); + } + + private string CoalesceVariables(string input) + { + bool insideVariable = false; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < input.Length; i++) + { + if (input[i] == '{') + { + insideVariable = true; + } + else if (input[i] == '}') + { + insideVariable = false; + sb.Append("?"); + } + else + { + sb.Append(input[i]); + } + } + return sb.ToString(); + } + + private void HandleClient(System.Net.Sockets.TcpClient client) + { + // WebServerProcessRequestEventArgs e = new WebServerProcessRequestEventArgs(); + // OnClientConnected(e); + /* + if (e.Cancel) + { + client.Close(); + return; + } + */ + + StreamReader sr = new StreamReader(client.GetStream()); + string line = sr.ReadLine(); + if (line == null) + { + Console.Error.WriteLine("unexpected NULL line from client"); + client.Close(); + return; + } + string[] lineParts = line.Split(new char[] { ' ' }); + if (lineParts.Length != 3) + { + Console.Error.WriteLine("unexpected request from client:\n----> {0}", line); + client.Close(); + return; + } + + string requestMethod = lineParts[0]; + string path = lineParts[1]; + string version = lineParts[2]; + + WebHeaderCollection headers = new WebHeaderCollection(); + while (true) + { + string headerLine = sr.ReadLine(); + if (String.IsNullOrEmpty(headerLine)) + break; + + string[] headerParts = headerLine.Split(new char[] { ':' }, 2); + if (headerParts.Length != 2) + continue; + + headers[headerParts[0]] = headerParts[1]; + } + + if (headers[HttpRequestHeader.Cookie] != null) + { + string cookie = headers[HttpRequestHeader.Cookie]; + } + + bool found = false; + Dictionary pathVariables = new Dictionary(); + WebContext context = new WebContext((WebApplication)Application.Instance, new WebRequest(version, requestMethod, path, headers, pathVariables), new WebResponse()); + context.Response.Cookies.Add("JSESSIONID", "0068F5AD24A89AB4AFCC4057F619EADF.authgwy-prod-mzzygbiy.prod-ui-auth.pr502.cust.pdx.wd", WebCookieScope.FromPath("/"), WebCookieSecurity.Secure | WebCookieSecurity.HttpOnly, WebCookieSameSite.None); + + WebServerProcessRequestEventArgs e = new WebServerProcessRequestEventArgs(client, context); + OnProcessRequest(e); + if (e.Handled) + { + WriteResponse(context, client.GetStream()); + client.Close(); + return; + } + + List sortedRoutes = new List(Routes); + sortedRoutes.Sort(new Comparison((left, right) => CoalesceVariables(right.PathTemplate).Length.CompareTo(CoalesceVariables(left.PathTemplate).Length))); + foreach (WebRoute route in sortedRoutes) + { + // !!! FIXME !!! /super/d/~/super/d/login.htmld/... falsely maps to ~/{tenant} route + // where {tenant} is super/d/~/super/d/login.htmld ...... ??? + // which... technically I guess it *should*, but... how do we get it to do what we WANT? + + if (route.Matches(path, pathVariables)) + { + route.Handler.ProcessRequest(context); + + WriteResponse(context, client.GetStream()); + found = true; + break; + } + } + + if (!found) + { + context.Response.ResponseCode = 404; + context.Response.ResponseText = "Not Found"; + WriteResponse(context, client.GetStream()); + } + + client.Close(); + } + private void tClientThread_ParameterizedThreadStart(object parm) + { + System.Net.Sockets.TcpClient client = (System.Net.Sockets.TcpClient)parm; + Console.WriteLine("Client connected"); + + try + { + HandleClient(client); + } + catch (System.Net.Sockets.SocketException ex) + { + Console.Error.WriteLine("caught SocketException; ignoring"); + } + catch (System.IO.IOException ex) + { + Console.Error.WriteLine("caught IOException; ignoring"); + } + } + + private bool _Stopping = false; + private Thread? tServer = null; + public void Start() + { + if (tServer != null) + throw new InvalidOperationException("Server already started; please call Stop() first"); + + _Stopping = false; + + tServer = new Thread(tServer_ThreadStart); + tServer.Start(); + } + public void Stop() + { + if (tServer == null) + throw new InvalidOperationException("Server not started; please call Start() first"); + + _Stopping = true; + } +} diff --git a/src/lib/MBS.Web/WebServerCreatedEventArgs.cs b/src/lib/MBS.Web/WebServerCreatedEventArgs.cs new file mode 100644 index 0000000..8a5b3c5 --- /dev/null +++ b/src/lib/MBS.Web/WebServerCreatedEventArgs.cs @@ -0,0 +1,16 @@ +using System.ComponentModel; +using System.Net; +using MBS.Core; +using MBS.Web.UI; + +namespace MBS.Web; + +public class WebServerCreatedEventArgs : EventArgs +{ + public WebServer Server { get; } + public WebServerCreatedEventArgs(WebServer server) + { + Server = server; + } + +} diff --git a/src/lib/MBS.Web/WebServerProcessRequestEventArgs.cs b/src/lib/MBS.Web/WebServerProcessRequestEventArgs.cs new file mode 100644 index 0000000..f9985aa --- /dev/null +++ b/src/lib/MBS.Web/WebServerProcessRequestEventArgs.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using System.Net; +using MBS.Core; +using MBS.Web.UI; + +namespace MBS.Web; + +public class WebServerProcessRequestEventArgs : EventArgs +{ + public WebContext Context { get; } + public bool Handled { get; set; } = false; + + public WebServerProcessRequestEventArgs(System.Net.Sockets.TcpClient client, WebContext context) + { + Context = context; + } + +} diff --git a/src/lib/MBS.Web/WebSession.cs b/src/lib/MBS.Web/WebSession.cs new file mode 100644 index 0000000..0c25911 --- /dev/null +++ b/src/lib/MBS.Web/WebSession.cs @@ -0,0 +1,7 @@ +namespace MBS.Web; + +public class WebSession +{ + + +}