Initial commit

This commit is contained in:
Michael Becker 2024-07-20 23:44:59 -04:00
parent 6b76c667b7
commit 21fa2c5af0
36 changed files with 1612 additions and 0 deletions

View File

@ -0,0 +1,40 @@
//
// ClientConnectedEventArgs.cs
//
// Author:
// Michael Becker <alcexhim@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
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;
}
}

View File

@ -0,0 +1,33 @@
//
// ClientConnectedEventArgs.cs
//
// Author:
// Michael Becker <alcexhim@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
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;
}
}

100
src/lib/MBS.Web/Control.cs Normal file
View File

@ -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<Control>
{
}
public Dictionary<string, string> PathVariables { get; } = new Dictionary<string, string>();
public Control(Dictionary<string, string>? pathVariables = null)
{
if (pathVariables == null)
pathVariables = new Dictionary<string, string>();
PathVariables = pathVariables;
}
protected virtual void OnInit(RenderEventArgs e)
{
}
protected virtual void RenderContents(XmlWriter writer)
{
}
protected virtual string TagName { get; } = "div";
public Dictionary<string, string> Attributes { get; } = new Dictionary<string, string>();
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<string, string> attrs = GetControlAttributes();
foreach (KeyValuePair<string, string> kvp in attrs)
{
string value = kvp.Value;
if (kvp.Key == "class")
{
IEnumerable<string> 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<string, string> GetControlAttributes()
{
return Attributes;
}
}

View File

@ -0,0 +1,48 @@
namespace MBS.Web;
public class CssClassList : List<string>
{
//
// 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<string> 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());
}
}
}

View File

@ -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<string, string> kvp in context.Request.PathVariables)
{
actualUrl = actualUrl.Replace("{" + kvp.Key + "}", kvp.Value);
}
context.Response.Headers.Add("Location", actualUrl);
}
}

View File

@ -0,0 +1,6 @@
namespace MBS.Web;
public interface IWebHandler
{
void ProcessRequest(WebContext context);
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\..\..\..\framework-dotnet\framework-dotnet\src\lib\MBS.Core\MBS.Core.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -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;
}
}

View File

@ -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<KeyValuePair<string, string>> attributes, IList<string> classes, string? content = null)
{
_tagName = tagName;
Attributes.AddRange(attributes);
ClassList.AddRange(classes);
Content = content;
}
public HtmlGenericControl(string tagName, IEnumerable<KeyValuePair<string, string>> attributes, string? content = null)
{
_tagName = tagName;
Attributes.AddRange(attributes);
Content = content;
}
public HtmlGenericControl(string tagName, IEnumerable<string> 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);
}
}
}

View File

@ -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<string, string> GetControlAttributes()
{
IDictionary<string, string> list = base.GetControlAttributes();
list.Add("rel", Rel);
list.Add("type", ContentType);
list.Add("href", TargetUrl);
return list;
}
protected override string TagName => "link";
}

View File

@ -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<string, string> GetControlAttributes()
{
IDictionary<string, string> dict = base.GetControlAttributes();
dict["id"] = ClientId;
return dict;
}
}

View File

@ -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);
}
}

View File

@ -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<string, string> GetControlAttributes()
{
IDictionary<string, string> attrs = base.GetControlAttributes();
if (UseSubmitBehavior)
{
attrs["type"] = "submit";
// attrs["value"] = GetTextWithoutMnemonic();
}
// else
{
if (HasMnemonic)
{
attrs["accesskey"] = MnemonicChar.ToString();
}
}
return attrs;
}
}

View File

@ -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<Control> GetChildControls()
{
return Controls;
}
}

View File

@ -0,0 +1,28 @@
using System.Xml;
namespace MBS.Web.UI.WebControls;
public abstract class ContainerBase : WebControl
{
private List<Control> _list = new List<Control>();
protected virtual IList<Control> GetChildControls()
{
return _list;
}
private IList<Control>? _childControls = null;
protected override void RenderContents(XmlWriter writer)
{
base.RenderContents(writer);
if (_childControls == null)
{
_childControls = GetChildControls();
}
foreach (Control control in _childControls)
{
control.Render(writer);
}
}
}

View File

@ -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<FormViewItem>
{
}
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<Control> GetChildControls()
{
List<Control> list = new List<Control>();
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<FormViewItem>? items = null)
{
if (items != null)
{
Items.AddRange(items);
}
}
}

View File

@ -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);
}
}

View File

@ -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<Control> HeaderControls { get; } = new List<Control>();
public List<Control> ContentControls { get; } = new List<Control>();
public List<Control> FooterControls { get; } = new List<Control>();
protected override string TagName => "div";
protected override IList<Control> GetChildControls()
{
List<Control> actual = new List<Control>();
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;
}
}

View File

@ -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<string, string> GetControlAttributes()
{
IDictionary<string, string> dict = base.GetControlAttributes();
dict["type"] = GetTextBoxType();
dict["name"] = Name;
return dict;
}
}

View File

@ -0,0 +1,72 @@
using System.Xml;
using MBS.Web.UI.HtmlControls;
namespace MBS.Web.UI;
public class WebPage : Control
{
public WebPage(Dictionary<string, string>? 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<Control> ctls = new List<Control>();
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<string, string>[] { new KeyValuePair<string, string>("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();
}
}

View File

@ -0,0 +1,33 @@
namespace MBS.Web.UI;
public class WebStyleSheet
{
public class WebStyleSheetCollection
: System.Collections.ObjectModel.Collection<WebStyleSheet>
{
}
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;
}
}

View File

@ -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<WebServerProcessRequestEventArgs> ProcessRequest;
protected virtual void OnProcessRequest(WebServerProcessRequestEventArgs e)
{
ProcessRequest?.Invoke(this, e);
}
private void server_OnProcessRequest(object sender, WebServerProcessRequestEventArgs e)
{
OnProcessRequest(e);
}
public event EventHandler<WebServerCreatedEventArgs> 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);
}
}
}

View File

@ -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; }
}

View File

@ -0,0 +1,92 @@
using System.Text;
namespace MBS.Web;
public class WebCookie
{
public class WebCookieCollection
: System.Collections.ObjectModel.Collection<WebCookie>
{
private Dictionary<string, WebCookie> _list = new Dictionary<string, WebCookie>();
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<string> parms = new List<string>();
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();
}
}

View File

@ -0,0 +1,8 @@
namespace MBS.Web;
public enum WebCookieSameSite
{
None = 0,
Strict = 1,
Lax = 2
}

View File

@ -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;
}

View File

@ -0,0 +1,9 @@
namespace MBS.Web;
[Flags()]
public enum WebCookieSecurity
{
None = 0,
Secure = 1,
HttpOnly = 2
}

View File

@ -0,0 +1,27 @@
namespace MBS.Web;
public class WebHandler : IWebHandler
{
public Action<WebContext>? Action { get; }
public WebHandler() : this(null) { }
public WebHandler(Action<WebContext>? 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);
}
}

View File

@ -0,0 +1,85 @@
using System.Net.Http.Headers;
using MBS.Core;
namespace MBS.Web;
public class WebHeaderCollection : List<KeyValuePair<string, string>>
{
private Dictionary<string, List<string>> _list = new Dictionary<string, List<string>>();
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<string>();
}
if (!_list[key].Contains(value))
{
_list[key].Add(value);
}
}
}
public IReadOnlyList<string> GetValues(string key)
{
return _list[key];
}
public void Add(string key, string value)
{
Add(new KeyValuePair<string, string>(key, value));
}
public new void Add(KeyValuePair<string, string> value)
{
if (!_list.ContainsKey(value.Key))
{
_list[value.Key] = new List<string>();
}
if (!_list[value.Key].Contains(value.Value))
{
_list[value.Key].Add(value.Value);
}
base.Add(value);
}
}

View File

@ -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<string, string> PathVariables { get; }
public WebHeaderCollection Headers { get; }
public WebRequest(string version, string method, string path, WebHeaderCollection headers, Dictionary<string, string> pathVariables)
{
Version = version;
Method = method;
Path = path;
if (Uri.TryCreate(path, UriKind.Relative, out Uri? uri))
{
Uri = uri;
}
Headers = headers;
PathVariables = pathVariables;
}
}

View File

@ -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));
}
}

142
src/lib/MBS.Web/WebRoute.cs Normal file
View File

@ -0,0 +1,142 @@
namespace MBS.Web;
public class WebRoute
{
public class WebRouteCollection
: System.Collections.ObjectModel.Collection<WebRoute>
{
}
public string PathTemplate { get; }
public IWebHandler Handler { get; }
public WebRoute(string pathTemplate, IWebHandler handler)
{
PathTemplate = pathTemplate;
Handler = handler;
}
public bool Matches(string pathstr, Dictionary<string, string> 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;
}
}

View File

@ -0,0 +1,261 @@
using System.Net;
using System.Text;
using MBS.Core;
using MBS.Web.UI;
namespace MBS.Web;
public class WebServer
{
/// <summary>
/// A <see cref="Dictionary" /> of key/value pairs to send along with every request.
/// </summary>
/// <typeparam name="string"></typeparam>
/// <typeparam name="string"></typeparam>
/// <returns></returns>
public Dictionary<string, string> Headers { get; } = new Dictionary<string, string>();
public Dictionary<string, WebSession> Sessions { get; } = new Dictionary<string, WebSession>();
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<WebServerProcessRequestEventArgs> 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<string, string> header in Headers)
{
sw.WriteLine(String.Format("{0}: {1}", header.Key, header.Value));
}
foreach (KeyValuePair<string, string> 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<string, string> pathVariables = new Dictionary<string, string>();
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<WebRoute> sortedRoutes = new List<WebRoute>(Routes);
sortedRoutes.Sort(new Comparison<WebRoute>((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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,7 @@
namespace MBS.Web;
public class WebSession
{
}