Got at least one data fetching method working; turns out, we can't use a patched LogicStack to get the data

This commit is contained in:
2026-01-14 22:11:11 +01:00
parent 40a8431464
commit 3f7122d30a
350 changed files with 41444 additions and 119 deletions

243
Vendor/EmbedIO-3.5.2/Net/CookieList.cs vendored Normal file
View File

@@ -0,0 +1,243 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Text;
using EmbedIO.Internal;
using EmbedIO.Net.Internal;
using EmbedIO.Utilities;
namespace EmbedIO.Net
{
/// <summary>
/// <para>Provides a collection container for instances of <see cref="Cookie"/>.</para>
/// <para>This class is meant to be used internally by EmbedIO; you don't need to
/// use this class directly.</para>
/// </summary>
#pragma warning disable CA1710 // Rename class to end in 'Collection' - it ends in 'List', i.e. 'Indexed Collection'.
public sealed class CookieList : List<Cookie>, ICookieCollection
#pragma warning restore CA1710
{
/// <inheritdoc />
public bool IsSynchronized => false;
/// <inheritdoc />
public Cookie? this[string name]
{
get
{
if (name == null)
throw new ArgumentNullException(nameof(name));
if (Count == 0)
return null;
var list = new List<Cookie>(this);
list.Sort(CompareCookieWithinSorted);
return list.FirstOrDefault(cookie => cookie.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary>Creates a <see cref="CookieList"/> by parsing
/// the value of one or more <c>Cookie</c> or <c>Set-Cookie</c> headers.</summary>
/// <param name="headerValue">The value, or comma-separated list of values,
/// of the header or headers.</param>
/// <returns>A newly-created instance of <see cref="CookieList"/>.</returns>
public static CookieList Parse(string headerValue)
{
var cookies = new CookieList();
Cookie? cookie = null;
var pairs = SplitCookieHeaderValue(headerValue);
for (var i = 0; i < pairs.Length; i++)
{
var pair = pairs[i].Trim();
if (pair.Length == 0)
continue;
if (pair.StartsWith("version", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Version = int.Parse(GetValue(pair, true), CultureInfo.InvariantCulture);
}
else if (pair.StartsWith("expires", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
var buff = new StringBuilder(GetValue(pair), 32);
if (i < pairs.Length - 1)
buff.AppendFormat(CultureInfo.InvariantCulture, ", {0}", pairs[++i].Trim());
if (!HttpDate.TryParse(buff.ToString(), out var expires))
expires = DateTimeOffset.Now;
if (cookie.Expires == DateTime.MinValue)
cookie.Expires = expires.LocalDateTime;
}
else if (pair.StartsWith("max-age", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
var max = int.Parse(GetValue(pair, true), CultureInfo.InvariantCulture);
cookie.Expires = DateTime.Now.AddSeconds(max);
}
else if (pair.StartsWith("path", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Path = GetValue(pair);
}
else if (pair.StartsWith("domain", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Domain = GetValue(pair);
}
else if (pair.StartsWith("port", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Port = pair.Equals("port", StringComparison.OrdinalIgnoreCase)
? "\"\""
: GetValue(pair);
}
else if (pair.StartsWith("comment", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Comment = WebUtility.UrlDecode(GetValue(pair));
}
else if (pair.StartsWith("commenturl", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.CommentUri = UriUtility.StringToUri(GetValue(pair, true));
}
else if (pair.StartsWith("discard", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Discard = true;
}
else if (pair.StartsWith("secure", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.Secure = true;
}
else if (pair.StartsWith("httponly", StringComparison.OrdinalIgnoreCase) && cookie != null)
{
cookie.HttpOnly = true;
}
else
{
if (cookie != null)
cookies.Add(cookie);
cookie = ParseCookie(pair);
}
}
if (cookie != null)
cookies.Add(cookie);
return cookies;
}
/// <inheritdoc />
public new void Add(Cookie cookie)
{
if (cookie == null)
throw new ArgumentNullException(nameof(cookie));
var pos = SearchCookie(cookie);
if (pos == -1)
{
base.Add(cookie);
return;
}
this[pos] = cookie;
}
/// <inheritdoc />
public void CopyTo(Array array, int index)
{
if (array == null)
throw new ArgumentNullException(nameof(array));
if (index < 0)
throw new ArgumentOutOfRangeException(nameof(index), "Less than zero.");
if (array.Rank > 1)
throw new ArgumentException("Multidimensional.", nameof(array));
if (array.Length - index < Count)
{
throw new ArgumentException(
"The number of elements in this collection is greater than the available space of the destination array.");
}
if (array.GetType().GetElementType()?.IsAssignableFrom(typeof(Cookie)) != true)
{
throw new InvalidCastException(
"The elements in this collection cannot be cast automatically to the type of the destination array.");
}
((IList) this).CopyTo(array, index);
}
private static string? GetValue(string nameAndValue, bool unquote = false)
{
var idx = nameAndValue.IndexOf('=');
if (idx < 0 || idx == nameAndValue.Length - 1)
return null;
var val = nameAndValue.Substring(idx + 1).Trim();
return unquote ? val.Unquote() : val;
}
private static string[] SplitCookieHeaderValue(string value) => value.SplitHeaderValue(true).ToArray();
private static int CompareCookieWithinSorted(Cookie x, Cookie y)
{
var ret = x.Version - y.Version;
return ret != 0
? ret
: (ret = string.Compare(x.Name, y.Name, StringComparison.Ordinal)) != 0
? ret
: y.Path.Length - x.Path.Length;
}
private static Cookie ParseCookie(string pair)
{
string name;
var val = string.Empty;
var pos = pair.IndexOf('=');
if (pos == -1)
{
name = pair;
}
else if (pos == pair.Length - 1)
{
name = pair.Substring(0, pos).TrimEnd(' ');
}
else
{
name = pair.Substring(0, pos).TrimEnd(' ');
val = pair.Substring(pos + 1).TrimStart(' ');
}
return new Cookie(name, val);
}
private int SearchCookie(Cookie cookie)
{
var name = cookie.Name;
var path = cookie.Path;
var domain = cookie.Domain;
var ver = cookie.Version;
for (var i = Count - 1; i >= 0; i--)
{
var c = this[i];
if (c.Name.Equals(name, StringComparison.OrdinalIgnoreCase) &&
c.Path.Equals(path, StringComparison.OrdinalIgnoreCase) &&
c.Domain.Equals(domain, StringComparison.OrdinalIgnoreCase) &&
c.Version == ver)
return i;
}
return -1;
}
}
}

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using EmbedIO.Net.Internal;
using Swan.Logging;
namespace EmbedIO.Net
{
/// <summary>
/// Represents the EndPoint Manager.
/// </summary>
public static class EndPointManager
{
private static readonly ConcurrentDictionary<IPAddress, ConcurrentDictionary<int, EndPointListener>> IPToEndpoints = new ();
/// <summary>
/// Gets or sets a value indicating whether [use IPv6]. By default, this flag is set.
/// </summary>
/// <value>
/// <c>true</c> if [use IPv6]; otherwise, <c>false</c>.
/// </value>
public static bool UseIpv6 { get; set; } = true;
internal static void AddListener(HttpListener listener)
{
var added = new List<string>();
try
{
foreach (var prefix in listener.Prefixes)
{
AddPrefix(prefix, listener);
added.Add(prefix);
}
}
catch (Exception ex)
{
ex.Log(nameof(AddListener));
foreach (var prefix in added)
{
RemovePrefix(prefix, listener);
}
throw;
}
}
internal static void RemoveEndPoint(EndPointListener epl, IPEndPoint ep)
{
if (IPToEndpoints.TryGetValue(ep.Address, out var p))
{
if (p.TryRemove(ep.Port, out _) && p.Count == 0)
{
_ = IPToEndpoints.TryRemove(ep.Address, out _);
}
}
epl.Dispose();
}
internal static void RemoveListener(HttpListener listener)
{
foreach (var prefix in listener.Prefixes)
{
RemovePrefix(prefix, listener);
}
}
internal static void AddPrefix(string p, HttpListener listener)
{
var lp = new ListenerPrefix(p);
if (!lp.IsValid())
{
throw new HttpListenerException(400, "Invalid path.");
}
// listens on all the interfaces if host name cannot be parsed by IPAddress.
var epl = GetEpListener(lp.Host, lp.Port, listener, lp.Secure);
epl.AddPrefix(lp, listener);
}
private static EndPointListener GetEpListener(string host, int port, HttpListener listener, bool secure = false)
{
var address = ResolveAddress(host);
var p = IPToEndpoints.GetOrAdd(address, x => new ConcurrentDictionary<int, EndPointListener>());
return p.GetOrAdd(port, x => new EndPointListener(listener, address, x, secure));
}
private static IPAddress ResolveAddress(string host)
{
if (host == "*" || host == "+" || host == "0.0.0.0")
{
return UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any;
}
if (IPAddress.TryParse(host, out var address))
{
return address;
}
try
{
var hostEntry = new IPHostEntry {
HostName = host,
AddressList = Dns.GetHostAddresses(host),
};
return hostEntry.AddressList[0];
}
catch
{
return UseIpv6 ? IPAddress.IPv6Any : IPAddress.Any;
}
}
private static void RemovePrefix(string prefix, HttpListener listener)
{
try
{
var lp = new ListenerPrefix(prefix);
if (!lp.IsValid())
{
return;
}
var epl = GetEpListener(lp.Host, lp.Port, listener, lp.Secure);
epl.RemovePrefix(lp);
}
catch (SocketException)
{
// ignored
}
}
}
}

161
Vendor/EmbedIO-3.5.2/Net/HttpListener.cs vendored Normal file
View File

@@ -0,0 +1,161 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Net.Internal;
namespace EmbedIO.Net
{
/// <summary>
/// The EmbedIO implementation of the standard HTTP Listener class.
///
/// Based on MONO HttpListener class.
/// </summary>
/// <seealso cref="IDisposable" />
public sealed class HttpListener : IHttpListener
{
private readonly SemaphoreSlim _ctxQueueSem = new (0);
private readonly ConcurrentDictionary<string, HttpListenerContext> _ctxQueue;
private readonly ConcurrentDictionary<HttpConnection, object> _connections;
private readonly HttpListenerPrefixCollection _prefixes;
private bool _disposed;
/// <summary>
/// Initializes a new instance of the <see cref="HttpListener" /> class.
/// </summary>
/// <param name="certificate">The certificate.</param>
public HttpListener(X509Certificate? certificate = null)
{
Certificate = certificate;
_prefixes = new HttpListenerPrefixCollection(this);
_connections = new ConcurrentDictionary<HttpConnection, object>();
_ctxQueue = new ConcurrentDictionary<string, HttpListenerContext>();
}
/// <inheritdoc />
public bool IgnoreWriteExceptions { get; set; } = true;
/// <inheritdoc />
public bool IsListening { get; private set; }
/// <inheritdoc />
public string Name { get; } = "Unosquare HTTP Listener";
/// <inheritdoc />
public List<string> Prefixes => _prefixes.ToList();
/// <summary>
/// Gets the certificate.
/// </summary>
/// <value>
/// The certificate.
/// </value>
internal X509Certificate? Certificate { get; }
/// <inheritdoc />
public void Start()
{
if (IsListening)
{
return;
}
EndPointManager.AddListener(this);
IsListening = true;
}
/// <inheritdoc />
public void Stop()
{
IsListening = false;
Close(false);
}
/// <inheritdoc />
public void AddPrefix(string urlPrefix) => _prefixes.Add(urlPrefix);
/// <inheritdoc />
public void Dispose()
{
if (_disposed)
{
return;
}
Close(true);
_ctxQueueSem.Dispose();
_disposed = true;
}
/// <inheritdoc />
public async Task<IHttpContextImpl> GetContextAsync(CancellationToken cancellationToken)
{
while (true)
{
await _ctxQueueSem.WaitAsync(cancellationToken).ConfigureAwait(false);
foreach (var key in _ctxQueue.Keys)
{
if (_ctxQueue.TryRemove(key, out var context))
{
return context;
}
break;
}
}
}
internal void RegisterContext(HttpListenerContext context)
{
if (!_ctxQueue.TryAdd(context.Id, context))
{
throw new InvalidOperationException("Unable to register context");
}
_ = _ctxQueueSem.Release();
}
internal void UnregisterContext(HttpListenerContext context) => _ctxQueue.TryRemove(context.Id, out _);
internal void AddConnection(HttpConnection cnc) => _connections[cnc] = cnc;
internal void RemoveConnection(HttpConnection cnc) => _connections.TryRemove(cnc, out _);
private void Close(bool closeExisting)
{
EndPointManager.RemoveListener(this);
var keys = _connections.Keys;
var connections = new HttpConnection[keys.Count];
keys.CopyTo(connections, 0);
_connections.Clear();
var list = new List<HttpConnection>(connections);
for (var i = list.Count - 1; i >= 0; i--)
{
list[i].Close(true);
}
if (!closeExisting)
{
return;
}
while (!_ctxQueue.IsEmpty)
{
foreach (var key in _ctxQueue.Keys.ToArray())
{
if (_ctxQueue.TryGetValue(key, out var context))
{
context.Connection.Close(true);
}
}
}
}
}
}

View File

@@ -0,0 +1,429 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace EmbedIO.Net.Internal
{
internal sealed class EndPointListener : IDisposable
{
private readonly Dictionary<HttpConnection, HttpConnection> _unregistered;
private readonly IPEndPoint _endpoint;
private readonly Socket _sock;
private Dictionary<ListenerPrefix, HttpListener> _prefixes;
private List<ListenerPrefix>? _unhandled; // unhandled; host = '*'
private List<ListenerPrefix>? _all; // all; host = '+
public EndPointListener(HttpListener listener, IPAddress address, int port, bool secure)
{
Listener = listener;
Secure = secure;
_endpoint = new IPEndPoint(address, port);
_sock = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
if (address.AddressFamily == AddressFamily.InterNetworkV6 && EndPointManager.UseIpv6)
{
_sock.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
}
_sock.Bind(_endpoint);
_sock.Listen(500);
var args = new SocketAsyncEventArgs { UserToken = this };
args.Completed += OnAccept;
Socket? dummy = null;
Accept(_sock, args, ref dummy);
_prefixes = new Dictionary<ListenerPrefix, HttpListener>();
_unregistered = new Dictionary<HttpConnection, HttpConnection>();
}
internal HttpListener Listener { get; }
internal bool Secure { get; }
public bool BindContext(HttpListenerContext context)
{
var req = context.Request;
var listener = SearchListener(req.Url, out var prefix);
if (listener == null)
{
return false;
}
context.Listener = listener;
context.Connection.Prefix = prefix;
return true;
}
public void UnbindContext(HttpListenerContext context) => context.Listener.UnregisterContext(context);
public void Dispose()
{
_sock.Dispose();
List<HttpConnection> connections;
lock (_unregistered)
{
// Clone the list because RemoveConnection can be called from Close
connections = new List<HttpConnection>(_unregistered.Keys);
_unregistered.Clear();
}
foreach (var c in connections)
{
c.Dispose();
}
}
public void AddPrefix(ListenerPrefix prefix, HttpListener listener)
{
List<ListenerPrefix>? current;
List<ListenerPrefix> future;
if (prefix.Host == "*")
{
do
{
current = _unhandled;
// TODO: Should we clone the items?
future = current?.ToList() ?? new List<ListenerPrefix>();
prefix.Listener = listener;
AddSpecial(future, prefix);
}
while (Interlocked.CompareExchange(ref _unhandled, future, current) != current);
return;
}
if (prefix.Host == "+")
{
do
{
current = _all;
future = current?.ToList() ?? new List<ListenerPrefix>();
prefix.Listener = listener;
AddSpecial(future, prefix);
}
while (Interlocked.CompareExchange(ref _all, future, current) != current);
return;
}
Dictionary<ListenerPrefix, HttpListener> prefs, p2;
do
{
prefs = _prefixes;
if (prefs.ContainsKey(prefix))
{
if (prefs[prefix] != listener)
{
throw new HttpListenerException(400, $"There is another listener for {prefix}");
}
return;
}
p2 = prefs.ToDictionary(x => x.Key, x => x.Value);
p2[prefix] = listener;
}
while (Interlocked.CompareExchange(ref _prefixes, p2, prefs) != prefs);
}
public void RemovePrefix(ListenerPrefix prefix)
{
List<ListenerPrefix>? current;
List<ListenerPrefix> future;
if (prefix.Host == "*")
{
do
{
current = _unhandled;
future = current?.ToList() ?? new List<ListenerPrefix>();
if (!RemoveSpecial(future, prefix))
{
break; // Prefix not found
}
}
while (Interlocked.CompareExchange(ref _unhandled, future, current) != current);
CheckIfRemove();
return;
}
if (prefix.Host == "+")
{
do
{
current = _all;
future = current?.ToList() ?? new List<ListenerPrefix>();
if (!RemoveSpecial(future, prefix))
{
break; // Prefix not found
}
}
while (Interlocked.CompareExchange(ref _all, future, current) != current);
CheckIfRemove();
return;
}
Dictionary<ListenerPrefix, HttpListener> prefs, p2;
do
{
prefs = _prefixes;
var prefixKey = _prefixes.Keys.FirstOrDefault(p => p.Path == prefix.Path);
if (prefixKey is null)
{
break;
}
p2 = prefs.ToDictionary(x => x.Key, x => x.Value);
_ = p2.Remove(prefixKey);
}
while (Interlocked.CompareExchange(ref _prefixes, p2, prefs) != prefs);
CheckIfRemove();
}
internal void RemoveConnection(HttpConnection conn)
{
lock (_unregistered)
{
_ = _unregistered.Remove(conn);
}
}
private static void Accept(Socket socket, SocketAsyncEventArgs e, ref Socket? accepted)
{
e.AcceptSocket = null;
bool acceptPending;
try
{
acceptPending = socket.AcceptAsync(e);
}
catch
{
try
{
accepted?.Dispose();
}
catch
{
// ignored
}
accepted = null;
return;
}
if (!acceptPending)
{
ProcessAccept(e);
}
}
private static void ProcessAccept(SocketAsyncEventArgs args)
{
Socket? accepted = null;
if (args.SocketError == SocketError.Success)
{
accepted = args.AcceptSocket;
}
var epl = (EndPointListener)args.UserToken;
Accept(epl._sock, args, ref accepted);
if (accepted == null)
{
return;
}
if (epl.Secure && epl.Listener.Certificate == null)
{
accepted.Dispose();
return;
}
HttpConnection conn;
try
{
conn = new HttpConnection(accepted, epl);
}
catch
{
return;
}
lock (epl._unregistered)
{
epl._unregistered[conn] = conn;
}
_ = conn.BeginReadRequest();
}
private static void OnAccept(object sender, SocketAsyncEventArgs e) => ProcessAccept(e);
private static HttpListener? MatchFromList(string path, List<ListenerPrefix>? list, out ListenerPrefix? prefix)
{
prefix = null;
if (list == null)
{
return null;
}
HttpListener? bestMatch = null;
var bestLength = -1;
foreach (var p in list)
{
if (p.Path.Length < bestLength || !path.StartsWith(p.Path, StringComparison.Ordinal))
{
continue;
}
bestLength = p.Path.Length;
bestMatch = p.Listener;
prefix = p;
}
return bestMatch;
}
private static void AddSpecial(ICollection<ListenerPrefix> coll, ListenerPrefix prefix)
{
if (coll == null)
{
return;
}
if (coll.Any(p => p.Path == prefix.Path))
{
throw new HttpListenerException(400, "Prefix already in use.");
}
coll.Add(prefix);
}
private static bool RemoveSpecial(IList<ListenerPrefix> coll, ListenerPrefix prefix)
{
if (coll == null)
{
return false;
}
var c = coll.Count;
for (var i = 0; i < c; i++)
{
if (coll[i].Path != prefix.Path)
{
continue;
}
coll.RemoveAt(i);
return true;
}
return false;
}
private HttpListener? SearchListener(Uri uri, out ListenerPrefix? prefix)
{
prefix = null;
if (uri == null)
{
return null;
}
var host = uri.Host;
var port = uri.Port;
var path = WebUtility.UrlDecode(uri.AbsolutePath);
var pathSlash = path[path.Length - 1] == '/' ? path : path + "/";
HttpListener? bestMatch = null;
var bestLength = -1;
if (!string.IsNullOrEmpty(host))
{
var result = _prefixes;
foreach (var p in result.Keys)
{
if (p.Path.Length < bestLength)
{
continue;
}
if (p.Host != host || p.Port != port)
{
continue;
}
if (!path.StartsWith(p.Path, StringComparison.Ordinal) && !pathSlash.StartsWith(p.Path, StringComparison.Ordinal))
{
continue;
}
bestLength = p.Path.Length;
bestMatch = result[p];
prefix = p;
}
if (bestLength != -1)
{
return bestMatch;
}
}
var list = _unhandled;
bestMatch = MatchFromList(path, list, out prefix);
if (path != pathSlash && bestMatch == null)
{
bestMatch = MatchFromList(pathSlash, list, out prefix);
}
if (bestMatch != null)
{
return bestMatch;
}
list = _all;
bestMatch = MatchFromList(path, list, out prefix);
if (path != pathSlash && bestMatch == null)
{
bestMatch = MatchFromList(pathSlash, list, out prefix);
}
return bestMatch;
}
private void CheckIfRemove()
{
if (_prefixes.Count > 0)
{
return;
}
var list = _unhandled;
if (list != null && list.Count > 0)
{
return;
}
list = _all;
if (list != null && list.Count > 0)
{
return;
}
EndPointManager.RemoveEndPoint(this, _endpoint);
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Linq;
namespace EmbedIO.Net.Internal
{
internal static class HeaderUtility
{
public static string? GetCharset(string? contentType)
=> contentType?
.Split(';')
.Select(p => p.Trim())
.Where(part => part.StartsWith("charset", StringComparison.OrdinalIgnoreCase))
.Select(GetAttributeValue)
.FirstOrDefault();
public static string? GetAttributeValue(string nameAndValue)
{
var idx = nameAndValue.IndexOf('=');
return idx < 0 || idx == nameAndValue.Length - 1 ? null : nameAndValue.Substring(idx + 1).Trim().Unquote();
}
}
}

View File

@@ -0,0 +1,11 @@
namespace EmbedIO.Net.Internal
{
partial class HttpConnection
{
private enum InputState
{
RequestLine,
Headers,
}
}
}

View File

@@ -0,0 +1,12 @@
namespace EmbedIO.Net.Internal
{
partial class HttpConnection
{
private enum LineState
{
None,
Cr,
Lf,
}
}
}

View File

@@ -0,0 +1,416 @@
using System;
using System.IO;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace EmbedIO.Net.Internal
{
internal sealed partial class HttpConnection : IDisposable
{
private const int BufferSize = 8192;
private readonly Timer _timer;
private readonly EndPointListener _epl;
private Socket? _sock;
private MemoryStream? _ms;
private byte[]? _buffer;
private HttpListenerContext _context;
private StringBuilder? _currentLine;
private RequestStream? _iStream;
private ResponseStream? _oStream;
private bool _contextBound;
private int _sTimeout = 90000; // 90k ms for first request, 15k ms from then on
private HttpListener? _lastListener;
private InputState _inputState = InputState.RequestLine;
private LineState _lineState = LineState.None;
private int _position;
private string? _errorMessage;
public HttpConnection(Socket sock, EndPointListener epl)
{
_sock = sock;
_epl = epl;
IsSecure = epl.Secure;
LocalEndPoint = (IPEndPoint) sock.LocalEndPoint;
RemoteEndPoint = (IPEndPoint) sock.RemoteEndPoint;
Stream = new NetworkStream(sock, false);
if (IsSecure)
{
var sslStream = new SslStream(Stream, true);
try
{
sslStream.AuthenticateAsServer(epl.Listener.Certificate);
}
catch
{
CloseSocket();
throw;
}
Stream = sslStream;
}
_timer = new Timer(OnTimeout, null, Timeout.Infinite, Timeout.Infinite);
_context = null!; // Silence warning about uninitialized field - _context will be initialized by the Init method
Init();
}
public int Reuses { get; private set; }
public Stream Stream { get; }
public IPEndPoint LocalEndPoint { get; }
public IPEndPoint RemoteEndPoint { get; }
public bool IsSecure { get; }
public ListenerPrefix? Prefix { get; set; }
public void Dispose()
{
Close(true);
_timer.Dispose();
_sock?.Dispose();
_ms?.Dispose();
_iStream?.Dispose();
_oStream?.Dispose();
Stream?.Dispose();
_lastListener?.Dispose();
}
public async Task BeginReadRequest()
{
_buffer ??= new byte[BufferSize];
try
{
if (Reuses == 1)
{
_sTimeout = 15000;
}
_ = _timer.Change(_sTimeout, Timeout.Infinite);
var data = await Stream.ReadAsync(_buffer, 0, BufferSize).ConfigureAwait(false);
await OnReadInternal(data).ConfigureAwait(false);
}
catch
{
_ = _timer.Change(Timeout.Infinite, Timeout.Infinite);
CloseSocket();
Unbind();
}
}
public RequestStream GetRequestStream(long contentLength)
{
if (_iStream == null)
{
var buffer = _ms.ToArray();
var length = (int) _ms.Length;
_ms = null;
_iStream = new RequestStream(Stream, buffer, _position, length - _position, contentLength);
}
return _iStream;
}
public ResponseStream GetResponseStream() => _oStream ??= new ResponseStream(Stream, _context.HttpListenerResponse, _context.Listener?.IgnoreWriteExceptions ?? true);
internal void SetError(string message) => _errorMessage = message;
internal void ForceClose() => Close(true);
internal void Close(bool forceClose = false)
{
if (_sock != null)
{
_oStream?.Dispose();
_oStream = null;
}
if (_sock == null)
{
return;
}
forceClose = forceClose
|| !_context.Request.KeepAlive
|| _context.Response.Headers["connection"] == "close";
if (!forceClose)
{
if (_context.HttpListenerRequest.FlushInput())
{
Reuses++;
Unbind();
Init();
_ = BeginReadRequest();
return;
}
}
using (var s = _sock)
{
_sock = null;
try
{
s?.Shutdown(SocketShutdown.Both);
}
catch
{
// ignored
}
}
Unbind();
RemoveConnection();
}
private void Init()
{
_contextBound = false;
_iStream = null;
_oStream = null;
Prefix = null;
_ms = new MemoryStream();
_position = 0;
_inputState = InputState.RequestLine;
_lineState = LineState.None;
_context = new HttpListenerContext(this);
}
private void OnTimeout(object unused)
{
CloseSocket();
Unbind();
}
private async Task OnReadInternal(int offset)
{
_ = _timer.Change(Timeout.Infinite, Timeout.Infinite);
// Continue reading until full header is received.
// Especially important for multipart requests when the second part of the header arrives after a tiny delay
// because the web browser has to measure the content length first.
while (true)
{
try
{
await _ms.WriteAsync(_buffer, 0, offset).ConfigureAwait(false);
if (_ms.Length > 32768)
{
Close(true);
return;
}
}
catch
{
CloseSocket();
Unbind();
return;
}
if (offset == 0)
{
CloseSocket();
Unbind();
return;
}
if (ProcessInput(_ms))
{
if (_errorMessage is null)
{
_context.HttpListenerRequest.FinishInitialization();
}
if (_errorMessage != null || !_epl.BindContext(_context))
{
Close(true);
return;
}
var listener = _context.Listener;
if (_lastListener != listener)
{
RemoveConnection();
listener.AddConnection(this);
_lastListener = listener;
}
_contextBound = true;
listener.RegisterContext(_context);
return;
}
offset = await Stream.ReadAsync(_buffer, 0, BufferSize).ConfigureAwait(false);
}
}
private void RemoveConnection()
{
if (_lastListener != null)
{
_lastListener.RemoveConnection(this);
}
else
{
_epl.RemoveConnection(this);
}
}
// true -> done processing
// false -> need more input
private bool ProcessInput(MemoryStream ms)
{
var buffer = ms.ToArray();
var len = (int)ms.Length;
var used = 0;
while (true)
{
if (_errorMessage != null)
{
return true;
}
if (_position >= len)
{
break;
}
string? line;
try
{
line = ReadLine(buffer, _position, len - _position, out used);
_position += used;
}
catch
{
_errorMessage = "Bad request";
return true;
}
if (line == null)
{
break;
}
if (string.IsNullOrEmpty(line))
{
if (_inputState == InputState.RequestLine)
{
continue;
}
_currentLine = null;
return true;
}
if (_inputState == InputState.RequestLine)
{
_context.HttpListenerRequest.SetRequestLine(line);
_inputState = InputState.Headers;
}
else
{
try
{
_context.HttpListenerRequest.AddHeader(line);
}
catch (Exception e)
{
_errorMessage = e.Message;
return true;
}
}
}
if (used == len)
{
ms.SetLength(0);
_position = 0;
}
return false;
}
private string? ReadLine(byte[] buffer, int offset, int len, out int used)
{
_currentLine ??= new StringBuilder(128);
var last = offset + len;
used = 0;
for (var i = offset; i < last && _lineState != LineState.Lf; i++)
{
used++;
var b = buffer[i];
switch (b)
{
case 13:
_lineState = LineState.Cr;
break;
case 10:
_lineState = LineState.Lf;
break;
default:
_ = _currentLine.Append((char)b);
break;
}
}
if (_lineState != LineState.Lf)
{
return null;
}
_lineState = LineState.None;
var result = _currentLine.ToString();
_currentLine.Length = 0;
return result;
}
private void Unbind()
{
if (!_contextBound)
{
return;
}
_epl.UnbindContext(_context);
_contextBound = false;
}
private void CloseSocket()
{
if (_sock == null)
{
return;
}
try
{
_sock.Dispose();
}
finally
{
_sock = null;
}
RemoveConnection();
}
}
}

View File

@@ -0,0 +1,129 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Authentication;
using EmbedIO.Internal;
using EmbedIO.Routing;
using EmbedIO.Sessions;
using EmbedIO.Utilities;
using EmbedIO.WebSockets;
using EmbedIO.WebSockets.Internal;
using Swan.Logging;
namespace EmbedIO.Net.Internal
{
// Provides access to the request and response objects used by the HttpListener class.
internal sealed class HttpListenerContext : IHttpContextImpl
{
private readonly Lazy<IDictionary<object, object>> _items = new (() => new Dictionary<object, object>(), true);
private readonly TimeKeeper _ageKeeper = new ();
private readonly Stack<Action<IHttpContext>> _closeCallbacks = new ();
private bool _closed;
internal HttpListenerContext(HttpConnection cnc)
{
Connection = cnc;
HttpListenerRequest = new HttpListenerRequest(this);
User = Auth.NoUser;
HttpListenerResponse = new HttpListenerResponse(this);
Id = UniqueIdGenerator.GetNext();
LocalEndPoint = Request.LocalEndPoint;
RemoteEndPoint = Request.RemoteEndPoint;
Route = RouteMatch.None;
Session = SessionProxy.None;
}
public string Id { get; }
public CancellationToken CancellationToken { get; set; }
public long Age => _ageKeeper.ElapsedTime;
public IPEndPoint LocalEndPoint { get; }
public IPEndPoint RemoteEndPoint { get; }
public IHttpRequest Request => HttpListenerRequest;
public RouteMatch Route { get; set; }
public string RequestedPath => Route.SubPath ?? string.Empty; // It will never be empty, because modules are matched via base routes - this is just to silence a warning.
public IHttpResponse Response => HttpListenerResponse;
public IPrincipal User { get; set; }
public ISessionProxy Session { get; set; }
public bool SupportCompressedRequests { get; set; }
public IDictionary<object, object> Items => _items.Value;
public bool IsHandled { get; private set; }
public MimeTypeProviderStack MimeTypeProviders { get; } = new MimeTypeProviderStack();
internal HttpListenerRequest HttpListenerRequest { get; }
internal HttpListenerResponse HttpListenerResponse { get; }
internal HttpListener? Listener { get; set; }
internal HttpConnection Connection { get; }
public void SetHandled() => IsHandled = true;
public void OnClose(Action<IHttpContext> callback)
{
if (_closed)
{
throw new InvalidOperationException("HTTP context has already been closed.");
}
_closeCallbacks.Push(Validate.NotNull(nameof(callback), callback));
}
public void Close()
{
_closed = true;
// Always close the response stream no matter what.
Response.Close();
foreach (var callback in _closeCallbacks)
{
try
{
callback(this);
}
catch (Exception e)
{
e.Log("HTTP context", $"[{Id}] Exception thrown by a HTTP context close callback.");
}
}
}
public async Task<IWebSocketContext> AcceptWebSocketAsync(
IEnumerable<string> requestedProtocols,
string acceptedProtocol,
int receiveBufferSize,
TimeSpan keepAliveInterval,
CancellationToken cancellationToken)
{
var webSocket = await WebSocket.AcceptAsync(this, acceptedProtocol).ConfigureAwait(false);
return new WebSocketContext(this, WebSocket.SupportedVersion, requestedProtocols, acceptedProtocol, webSocket, cancellationToken);
}
public string GetMimeType(string extension)
=> MimeTypeProviders.GetMimeType(extension);
public bool TryDetermineCompression(string mimeType, out bool preferCompression)
=> MimeTypeProviders.TryDetermineCompression(mimeType, out preferCompression);
}
}

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace EmbedIO.Net.Internal
{
internal class HttpListenerPrefixCollection : List<string>
{
private readonly HttpListener _listener;
internal HttpListenerPrefixCollection(HttpListener listener)
{
_listener = listener;
}
public new void Add(string uriPrefix)
{
ListenerPrefix.CheckUri(uriPrefix);
if (Contains(uriPrefix))
{
return;
}
base.Add(uriPrefix);
if (_listener.IsListening)
{
EndPointManager.AddPrefix(uriPrefix, _listener);
}
}
}
}

View File

@@ -0,0 +1,497 @@
using System;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using EmbedIO.Internal;
using EmbedIO.Utilities;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents an HTTP Listener Request.
/// </summary>
internal sealed partial class HttpListenerRequest : IHttpRequest
{
private static readonly byte[] HttpStatus100 = WebServer.DefaultEncoding.GetBytes("HTTP/1.1 100 Continue\r\n\r\n");
private static readonly char[] Separators = { ' ' };
private readonly HttpConnection _connection;
private CookieList? _cookies;
private Stream? _inputStream;
private bool _kaSet;
private bool _keepAlive;
internal HttpListenerRequest(HttpListenerContext context)
{
_connection = context.Connection;
}
/// <summary>
/// Gets the MIME accept types.
/// </summary>
/// <value>
/// The accept types.
/// </value>
public string[] AcceptTypes { get; private set; } = Array.Empty<string>();
/// <inheritdoc />
public Encoding ContentEncoding
{
get
{
if (!HasEntityBody || ContentType == null)
{
return WebServer.DefaultEncoding;
}
var charSet = HeaderUtility.GetCharset(ContentType);
if (string.IsNullOrEmpty(charSet))
{
return WebServer.DefaultEncoding;
}
try
{
return Encoding.GetEncoding(charSet);
}
catch (ArgumentException)
{
return WebServer.DefaultEncoding;
}
}
}
/// <inheritdoc />
public long ContentLength64 => long.TryParse(Headers[HttpHeaderNames.ContentLength], out var val) ? val : 0;
/// <inheritdoc />
public string ContentType => Headers[HttpHeaderNames.ContentType];
/// <inheritdoc />
public ICookieCollection Cookies => _cookies ??= new CookieList();
/// <inheritdoc />
public bool HasEntityBody => ContentLength64 > 0;
/// <inheritdoc />
public NameValueCollection Headers { get; } = new ();
/// <inheritdoc />
public string HttpMethod { get; private set; } = string.Empty;
/// <inheritdoc />
public HttpVerbs HttpVerb { get; private set; }
/// <inheritdoc />
public Stream InputStream => _inputStream ??= ContentLength64 > 0 ? _connection.GetRequestStream(ContentLength64) : Stream.Null;
/// <inheritdoc />
public bool IsAuthenticated => false;
/// <inheritdoc />
public bool IsLocal => LocalEndPoint.Address?.Equals(RemoteEndPoint.Address) ?? true;
/// <inheritdoc />
public bool IsSecureConnection => _connection.IsSecure;
/// <inheritdoc />
public bool KeepAlive
{
get
{
if (!_kaSet)
{
var cnc = Headers.GetValues(HttpHeaderNames.Connection);
_keepAlive = ProtocolVersion < HttpVersion.Version11
? cnc != null && cnc.Length == 1 && string.Compare(cnc[0], "keep-alive", StringComparison.OrdinalIgnoreCase) == 0
: cnc == null || cnc.All(s => string.Compare(s, "close", StringComparison.OrdinalIgnoreCase) != 0);
_kaSet = true;
}
return _keepAlive;
}
}
/// <inheritdoc />
public IPEndPoint LocalEndPoint => _connection.LocalEndPoint;
/// <inheritdoc />
public Version ProtocolVersion { get; private set; } = HttpVersion.Version11;
/// <inheritdoc />
public NameValueCollection QueryString { get; } = new ();
/// <inheritdoc />
public string RawUrl { get; private set; } = string.Empty;
/// <inheritdoc />
public IPEndPoint RemoteEndPoint => _connection.RemoteEndPoint;
/// <inheritdoc />
public Uri Url { get; private set; } = WebServer.NullUri;
/// <inheritdoc />
public Uri? UrlReferrer { get; private set; }
/// <inheritdoc />
public string UserAgent => Headers[HttpHeaderNames.UserAgent];
public string UserHostAddress => LocalEndPoint.ToString();
public string UserHostName => Headers[HttpHeaderNames.Host];
public string[] UserLanguages { get; private set; } = Array.Empty<string>();
/// <inheritdoc />
public bool IsWebSocketRequest
=> HttpVerb == HttpVerbs.Get
&& ProtocolVersion >= HttpVersion.Version11
&& Headers.Contains(HttpHeaderNames.Upgrade, "websocket")
&& Headers.Contains(HttpHeaderNames.Connection, "Upgrade");
internal void SetRequestLine(string req)
{
const string forbiddenMethodChars = "\"(),/:;<=>?@[\\]{}";
var parts = req.Split(Separators, 3);
if (parts.Length != 3)
{
_connection.SetError("Invalid request line (parts).");
return;
}
HttpMethod = parts[0];
foreach (var c in HttpMethod)
{
// See https://tools.ietf.org/html/rfc7230#section-3.2.6
// for the list of allowed characters
if (c < 32 || c >= 127 || forbiddenMethodChars.IndexOf(c) >= 0)
{
_connection.SetError("(Invalid verb)");
return;
}
}
HttpVerb = IsKnownHttpMethod(HttpMethod, out var verb) ? verb : HttpVerbs.Any;
RawUrl = parts[1];
if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/", StringComparison.Ordinal))
{
_connection.SetError("Invalid request line (missing HTTP version).");
return;
}
try
{
ProtocolVersion = new Version(parts[2].Substring(5));
if (ProtocolVersion.Major < 1)
{
throw new InvalidOperationException();
}
}
catch
{
_connection.SetError("Invalid request line (could not parse HTTP version).");
}
}
internal void FinishInitialization()
{
var host = UserHostName;
if (ProtocolVersion > HttpVersion.Version10 && string.IsNullOrEmpty(host))
{
_connection.SetError("Invalid host name");
return;
}
var rawUri = UriUtility.StringToAbsoluteUri(RawUrl.ToLowerInvariant());
var path = rawUri?.PathAndQuery ?? RawUrl;
if (string.IsNullOrEmpty(host))
{
host = rawUri?.Host ?? UserHostAddress;
}
var colonPos = host.LastIndexOf(':');
var closedSquareBracketPos = host.LastIndexOf(']');
if (colonPos >= 0 && closedSquareBracketPos < colonPos)
{
host = host.Substring(0, colonPos);
}
// var baseUri = $"{(IsSecureConnection ? "https" : "http")}://{host}:{LocalEndPoint.Port}";
var baseUri = $"http://{host}:{LocalEndPoint.Port}";
if (!Uri.TryCreate(baseUri + path, UriKind.Absolute, out var url))
{
_connection.SetError(WebUtility.HtmlEncode($"Invalid url: {baseUri}{path}"));
return;
}
Url = url;
InitializeQueryString(Url.Query);
if (ContentLength64 == 0 && (HttpVerb == HttpVerbs.Post || HttpVerb == HttpVerbs.Put))
{
return;
}
if (string.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0)
{
_connection.GetResponseStream().InternalWrite(HttpStatus100, 0, HttpStatus100.Length);
}
}
internal void AddHeader(string header)
{
var colon = header.IndexOf(':');
if (colon == -1 || colon == 0)
{
_connection.SetError("Bad Request");
return;
}
var name = header.Substring(0, colon).Trim();
var val = header.Substring(colon + 1).Trim();
Headers.Set(name, val);
switch (name.ToLowerInvariant())
{
case "accept-language":
UserLanguages = val.SplitByComma(); // yes, only split with a ','
break;
case "accept":
AcceptTypes = val.SplitByComma(); // yes, only split with a ','
break;
case "content-length":
Headers[HttpHeaderNames.ContentLength] = val.Trim();
if (ContentLength64 < 0)
{
_connection.SetError("Invalid Content-Length.");
}
break;
case "referer":
try
{
UrlReferrer = new Uri(val);
}
catch
{
UrlReferrer = null;
}
break;
case "cookie":
ParseCookies(val);
break;
}
}
// returns true is the stream could be reused.
internal bool FlushInput()
{
if (!HasEntityBody)
{
return true;
}
var length = 2048;
if (ContentLength64 > 0)
{
length = (int)Math.Min(ContentLength64, length);
}
var bytes = new byte[length];
while (true)
{
try
{
if (InputStream.Read(bytes, 0, length) <= 0)
{
return true;
}
}
catch (ObjectDisposedException)
{
_inputStream = null;
return true;
}
catch
{
return false;
}
}
}
// Optimized for the following list of methods:
// "DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"
// ***NOTE***: The verb parameter is NOT VALID upon exit if false is returned.
private static bool IsKnownHttpMethod(string method, out HttpVerbs verb)
{
switch (method.Length)
{
case 3:
switch (method[0])
{
case 'G':
verb = HttpVerbs.Get;
return method[1] == 'E' && method[2] == 'T';
case 'P':
verb = HttpVerbs.Put;
return method[1] == 'U' && method[2] == 'T';
default:
verb = HttpVerbs.Any;
return false;
}
case 4:
switch (method[0])
{
case 'H':
verb = HttpVerbs.Head;
return method[1] == 'E' && method[2] == 'A' && method[3] == 'D';
case 'P':
verb = HttpVerbs.Post;
return method[1] == 'O' && method[2] == 'S' && method[3] == 'T';
default:
verb = HttpVerbs.Any;
return false;
}
case 5:
verb = HttpVerbs.Patch;
return method[0] == 'P'
&& method[1] == 'A'
&& method[2] == 'T'
&& method[3] == 'C'
&& method[4] == 'H';
case 6:
verb = HttpVerbs.Delete;
return method[0] == 'D'
&& method[1] == 'E'
&& method[2] == 'L'
&& method[3] == 'E'
&& method[4] == 'T'
&& method[5] == 'E';
case 7:
verb = HttpVerbs.Options;
return method[0] == 'O'
&& method[1] == 'P'
&& method[2] == 'T'
&& method[3] == 'I'
&& method[4] == 'O'
&& method[5] == 'N'
&& method[6] == 'S';
default:
verb = HttpVerbs.Any;
return false;
}
}
private void ParseCookies(string val)
{
_cookies ??= new CookieList();
var cookieStrings = val.SplitByAny(';', ',')
.Where(x => !string.IsNullOrEmpty(x));
Cookie? current = null;
var version = 0;
foreach (var str in cookieStrings)
{
if (str.StartsWith("$Version", StringComparison.Ordinal))
{
version = int.Parse(str.Substring(str.IndexOf('=') + 1).Unquote(), CultureInfo.InvariantCulture);
}
else if (str.StartsWith("$Path", StringComparison.Ordinal) && current != null)
{
current.Path = str.Substring(str.IndexOf('=') + 1).Trim();
}
else if (str.StartsWith("$Domain", StringComparison.Ordinal) && current != null)
{
current.Domain = str.Substring(str.IndexOf('=') + 1).Trim();
}
else if (str.StartsWith("$Port", StringComparison.Ordinal) && current != null)
{
current.Port = str.Substring(str.IndexOf('=') + 1).Trim();
}
else
{
if (current != null)
{
_cookies.Add(current);
}
current = new Cookie();
var idx = str.IndexOf('=');
if (idx > 0)
{
current.Name = str.Substring(0, idx).Trim();
current.Value = str.Substring(idx + 1).Trim();
}
else
{
current.Name = str.Trim();
current.Value = string.Empty;
}
current.Version = version;
}
}
if (current != null)
{
_cookies.Add(current);
}
}
private void InitializeQueryString(string query)
{
if (string.IsNullOrEmpty(query))
{
return;
}
if (query[0] == '?')
{
query = query.Substring(1);
}
var components = query.Split('&');
foreach (var kv in components)
{
var pos = kv.IndexOf('=');
if (pos == -1)
{
QueryString.Add(null, WebUtility.UrlDecode(kv));
}
else
{
var key = WebUtility.UrlDecode(kv.Substring(0, pos));
var val = WebUtility.UrlDecode(kv.Substring(pos + 1));
QueryString.Add(key, val);
}
}
}
}
}

View File

@@ -0,0 +1,411 @@
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using EmbedIO.Utilities;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents an HTTP Listener's response.
/// </summary>
/// <seealso cref="IDisposable" />
internal sealed class HttpListenerResponse : IHttpResponse, IDisposable
{
private readonly HttpConnection _connection;
private readonly HttpListenerRequest _request;
private readonly string _id;
private bool _disposed;
private string _contentType = MimeType.Html; // Same default value as Microsoft's implementation
private CookieList? _cookies;
private bool _keepAlive;
private ResponseStream? _outputStream;
private int _statusCode = 200;
private bool _chunked;
internal HttpListenerResponse(HttpListenerContext context)
{
_request = context.HttpListenerRequest;
_connection = context.Connection;
_id = context.Id;
_keepAlive = context.Request.KeepAlive;
}
/// <inheritdoc />
public Encoding? ContentEncoding { get; set; } = WebServer.DefaultEncoding;
/// <inheritdoc />
/// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
/// <exception cref="InvalidOperationException">This property is being set and headers were already sent.</exception>
public long ContentLength64
{
get => Headers.ContainsKey(HttpHeaderNames.ContentLength) && long.TryParse(Headers[HttpHeaderNames.ContentLength], out var val) ? val : 0;
set
{
EnsureCanChangeHeaders();
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "Must be >= 0");
}
Headers[HttpHeaderNames.ContentLength] = value.ToString(CultureInfo.InvariantCulture);
}
}
/// <inheritdoc />
/// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
/// <exception cref="InvalidOperationException">This property is being set and headers were already sent.</exception>
/// <exception cref="ArgumentNullException">This property is being set to <see langword="null"/>.</exception>
/// <exception cref="ArgumentException">This property is being set to the empty string.</exception>
public string ContentType
{
get => _contentType;
set
{
EnsureCanChangeHeaders();
_contentType = Validate.NotNullOrEmpty(nameof(value), value);
}
}
/// <inheritdoc />
public ICookieCollection Cookies => CookieCollection;
/// <inheritdoc />
public WebHeaderCollection Headers { get; } = new WebHeaderCollection();
/// <inheritdoc />
public bool KeepAlive
{
get => _keepAlive;
set
{
EnsureCanChangeHeaders();
_keepAlive = value;
}
}
/// <inheritdoc />
public Stream OutputStream => _outputStream ??= _connection.GetResponseStream();
/// <inheritdoc />
public Version ProtocolVersion => _request.ProtocolVersion;
/// <inheritdoc />
/// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
/// <exception cref="InvalidOperationException">This property is being set and headers were already sent.</exception>
public bool SendChunked
{
get => _chunked;
set
{
EnsureCanChangeHeaders();
_chunked = value;
}
}
/// <inheritdoc />
/// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
/// <exception cref="InvalidOperationException">This property is being set and headers were already sent.</exception>
public int StatusCode
{
get => _statusCode;
set
{
EnsureCanChangeHeaders();
if (value < 100 || value > 999)
{
throw new ArgumentOutOfRangeException(nameof(StatusCode), "StatusCode must be between 100 and 999.");
}
_statusCode = value;
StatusDescription = HttpListenerResponseHelper.GetStatusDescription(value);
}
}
/// <inheritdoc />
public string StatusDescription { get; set; } = "OK";
internal CookieList CookieCollection
{
get => _cookies ??= new CookieList();
set => _cookies = value;
}
internal bool HeadersSent { get; set; }
void IDisposable.Dispose() => Close(true);
public void Close()
{
if (!_disposed)
{
Close(false);
}
}
/// <inheritdoc />
public void SetCookie(Cookie cookie)
{
if (cookie == null)
{
throw new ArgumentNullException(nameof(cookie));
}
if (_cookies != null)
{
if (_cookies.Any(c => cookie.Name == c.Name && cookie.Domain == c.Domain && cookie.Path == c.Path))
{
throw new ArgumentException("The cookie already exists.");
}
}
else
{
_cookies = new CookieList();
}
_cookies.Add(cookie);
}
internal MemoryStream SendHeaders(bool closing)
{
if (_contentType != null)
{
var contentTypeValue = _contentType.IndexOf("charset=", StringComparison.Ordinal) == -1 && ContentEncoding is not null
? $"{_contentType}; charset={ContentEncoding.WebName}"
: _contentType;
Headers.Add(HttpHeaderNames.ContentType, contentTypeValue);
}
if (Headers[HttpHeaderNames.Server] == null)
{
Headers.Add(HttpHeaderNames.Server, WebServer.Signature);
}
if (Headers[HttpHeaderNames.Date] == null)
{
Headers.Add(HttpHeaderNames.Date, HttpDate.Format(DateTime.UtcNow));
}
// HTTP did not support chunked transfer encoding before version 1.1;
// besides, there's no point in setting transfer encoding at all without a request body.
if (closing || ProtocolVersion < HttpVersion.Version11)
{
_chunked = false;
}
// Was content length set to a valid value, AND chunked encoding not set?
// Note that this does not mean that a response body _will_ be sent
// as this could be the response to a HEAD request.
var haveContentLength = !_chunked
&& Headers.ContainsKey(HttpHeaderNames.ContentLength)
&& long.TryParse(Headers[HttpHeaderNames.ContentLength], NumberStyles.None, CultureInfo.InvariantCulture, out var contentLength)
&& contentLength >= 0L;
if (!haveContentLength)
{
// Content length could have been set to an invalid value (e.g. "-1")
// so we must either force it to 0, or remove the header completely.
if (closing)
{
// Content length was not explicitly set to a valid value,
// and there is no request body.
Headers[HttpHeaderNames.ContentLength] = "0";
}
else
{
// Content length was not explicitly set to a valid value,
// and we're going to send a request body.
// - Remove possibly invalid Content-Length header
// - Enable chunked transfer encoding for HTTP 1.1
Headers.Remove(HttpHeaderNames.ContentLength);
if (ProtocolVersion >= HttpVersion.Version11)
{
_chunked = true;
}
}
}
if (_chunked)
{
Headers.Add(HttpHeaderNames.TransferEncoding, "chunked");
}
//// Apache forces closing the connection for these status codes:
//// HttpStatusCode.BadRequest 400
//// HttpStatusCode.RequestTimeout 408
//// HttpStatusCode.LengthRequired 411
//// HttpStatusCode.RequestEntityTooLarge 413
//// HttpStatusCode.RequestUriTooLong 414
//// HttpStatusCode.InternalServerError 500
//// HttpStatusCode.ServiceUnavailable 503
var reuses = _connection.Reuses;
var keepAlive = _statusCode switch {
400 => false,
408 => false,
411 => false,
413 => false,
414 => false,
500 => false,
503 => false,
_ => _keepAlive && reuses < 100
};
_keepAlive = keepAlive;
if (keepAlive)
{
Headers.Add(HttpHeaderNames.Connection, "keep-alive");
if (ProtocolVersion >= HttpVersion.Version11)
{
Headers.Add(HttpHeaderNames.KeepAlive, $"timeout=15,max={100 - reuses}");
}
}
else
{
Headers.Add(HttpHeaderNames.Connection, "close");
}
return WriteHeaders();
}
private static void AppendSetCookieHeader(StringBuilder sb, Cookie cookie)
{
if (cookie.Name.Length == 0)
{
return;
}
_ = sb.Append("Set-Cookie: ");
if (cookie.Version > 0)
{
_ = sb.Append("Version=").Append(cookie.Version).Append("; ");
}
_ = sb
.Append(cookie.Name)
.Append('=')
.Append(cookie.Value);
if (cookie.Expires != DateTime.MinValue)
{
_ = sb
.Append("; Expires=")
.Append(HttpDate.Format(cookie.Expires));
}
if (!string.IsNullOrEmpty(cookie.Path))
{
_ = sb.Append("; Path=").Append(QuotedString(cookie, cookie.Path));
}
if (!string.IsNullOrEmpty(cookie.Domain))
{
_ = sb.Append("; Domain=").Append(QuotedString(cookie, cookie.Domain));
}
if (!string.IsNullOrEmpty(cookie.Port))
{
_ = sb.Append("; Port=").Append(cookie.Port);
}
if (cookie.Secure)
{
_ = sb.Append("; Secure");
}
if (cookie.HttpOnly)
{
_ = sb.Append("; HttpOnly");
}
_ = sb.Append("\r\n");
}
private static string QuotedString(Cookie cookie, string value)
=> cookie.Version == 0 || value.IsToken() ? value : "\"" + value.Replace("\"", "\\\"") + "\"";
private void Close(bool force)
{
_disposed = true;
_connection.Close(force);
}
private string GetHeaderData()
{
var sb = new StringBuilder()
.Append("HTTP/")
.Append(ProtocolVersion)
.Append(' ')
.Append(_statusCode)
.Append(' ')
.Append(StatusDescription)
.Append("\r\n");
foreach (var key in Headers.AllKeys.Where(x => x != "Set-Cookie"))
{
_ = sb
.Append(key)
.Append(": ")
.Append(Headers[key])
.Append("\r\n");
}
if (_cookies != null)
{
foreach (var cookie in _cookies)
{
AppendSetCookieHeader(sb, cookie);
}
}
if (Headers.ContainsKey(HttpHeaderNames.SetCookie))
{
foreach (var cookie in CookieList.Parse(Headers[HttpHeaderNames.SetCookie]))
{
AppendSetCookieHeader(sb, cookie);
}
}
return sb.Append("\r\n").ToString();
}
private MemoryStream WriteHeaders()
{
var stream = new MemoryStream();
var data = WebServer.DefaultEncoding.GetBytes(GetHeaderData());
var preamble = WebServer.DefaultEncoding.GetPreamble();
stream.Write(preamble, 0, preamble.Length);
stream.Write(data, 0, data.Length);
_outputStream ??= _connection.GetResponseStream();
// Assumes that the ms was at position 0
stream.Position = preamble.Length;
HeadersSent = true;
return stream;
}
private void EnsureCanChangeHeaders()
{
if (_disposed)
{
throw new ObjectDisposedException(_id);
}
if (HeadersSent)
{
throw new InvalidOperationException("Header values cannot be changed after headers are sent.");
}
}
}
}

View File

@@ -0,0 +1,55 @@
namespace EmbedIO.Net.Internal
{
internal static class HttpListenerResponseHelper
{
internal static string GetStatusDescription(int code) => code switch {
100 => "Continue",
101 => "Switching Protocols",
102 => "Processing",
200 => "OK",
201 => "Created",
202 => "Accepted",
203 => "Non-Authoritative Information",
204 => "No Content",
205 => "Reset Content",
206 => "Partial Content",
207 => "Multi-Status",
300 => "Multiple Choices",
301 => "Moved Permanently",
302 => "Found",
303 => "See Other",
304 => "Not Modified",
305 => "Use Proxy",
307 => "Temporary Redirect",
400 => "Bad Request",
401 => "Unauthorized",
402 => "Payment Required",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
406 => "Not Acceptable",
407 => "Proxy Authentication Required",
408 => "Request Timeout",
409 => "Conflict",
410 => "Gone",
411 => "Length Required",
412 => "Precondition Failed",
413 => "Request Entity Too Large",
414 => "Request-Uri Too Long",
415 => "Unsupported Media Type",
416 => "Requested Range Not Satisfiable",
417 => "Expectation Failed",
422 => "Unprocessable Entity",
423 => "Locked",
424 => "Failed Dependency",
500 => "Internal Server Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
505 => "Http Version Not Supported",
507 => "Insufficient Storage",
_ => string.Empty
};
}
}

View File

@@ -0,0 +1,35 @@
using System;
namespace EmbedIO.Net.Internal
{
internal sealed class ListenerPrefix
{
public ListenerPrefix(string uri)
{
var parsedUri = ListenerUri.Parse(uri);
Secure = parsedUri.Secure;
Host = parsedUri.Host;
Port = parsedUri.Port;
Path = parsedUri.Path;
}
public HttpListener? Listener { get; set; }
public bool Secure { get; }
public string Host { get; }
public int Port { get; }
public string Path { get; }
public static void CheckUri(string uri)
{
_ = ListenerUri.Parse(uri);
}
public bool IsValid() => Path.IndexOf('%') == -1 && Path.IndexOf("//", StringComparison.Ordinal) == -1;
public override string ToString() => $"{Host}:{Port} ({(Secure ? "Secure" : "Insecure")}";
}
}

View File

@@ -0,0 +1,91 @@
using System;
namespace EmbedIO.Net.Internal
{
internal class ListenerUri
{
private ListenerUri(bool secure,
string host,
int port,
string path)
{
Secure = secure;
Host = host;
Port = port;
Path = path;
}
public bool Secure { get; private set; }
public string Host { get; private set; }
public int Port { get; private set; }
public string Path { get; private set; }
public static ListenerUri Parse(string uri)
{
bool secure;
int port;
int parsingPosition;
if (uri.StartsWith("http://"))
{
secure = false;
port = 80;
parsingPosition = "http://".Length;
}
else if (uri.StartsWith("https://"))
{
secure = true;
port = 443;
parsingPosition = "https://".Length;
}
else
{
throw new Exception("Only 'http' and 'https' schemes are supported.");
}
var startOfPath = uri.IndexOf('/', parsingPosition);
if (startOfPath == -1)
{
throw new ArgumentException("Path should end in '/'.");
}
var hostWithPort = uri.Substring(parsingPosition, startOfPath - parsingPosition);
var startOfPortWithColon = hostWithPort.LastIndexOf(':');
if (startOfPortWithColon > -1)
{
startOfPortWithColon += parsingPosition;
}
var endOfIpV6 = hostWithPort.LastIndexOf(']');
if (endOfIpV6 > -1)
{
endOfIpV6 += parsingPosition;
}
if (endOfIpV6 > startOfPortWithColon)
{
startOfPortWithColon = -1;
}
if (startOfPortWithColon != -1 && startOfPortWithColon < startOfPath)
{
if (!int.TryParse(uri.Substring(startOfPortWithColon + 1, startOfPath - startOfPortWithColon - 1), out port) || port <= 0 || port >= 65535)
{
throw new ArgumentException("Invalid port.");
}
}
var host = uri.Substring(parsingPosition, (startOfPortWithColon == -1 ? startOfPath : startOfPortWithColon) - parsingPosition);
var path = uri.Substring(startOfPath);
if (!path.EndsWith("/"))
{
throw new ArgumentException("Path should end in '/'.");
}
return new ListenerUri(secure, host, port, path);
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Linq;
using Swan;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents some System.NET custom extensions.
/// </summary>
internal static class NetExtensions
{
internal static byte[] ToByteArray(this ushort value, Endianness order)
{
var bytes = BitConverter.GetBytes(value);
if (!order.IsHostOrder())
{
Array.Reverse(bytes);
}
return bytes;
}
internal static byte[] ToByteArray(this ulong value, Endianness order)
{
var bytes = BitConverter.GetBytes(value);
if (!order.IsHostOrder())
{
Array.Reverse(bytes);
}
return bytes;
}
internal static byte[] ToHostOrder(this byte[] source, Endianness sourceOrder)
=> source.Length < 1 ? source
: sourceOrder.IsHostOrder() ? source
: source.Reverse().ToArray();
// true: !(true ^ true) or !(false ^ false)
// false: !(true ^ false) or !(false ^ true)
private static bool IsHostOrder(this Endianness order)
=> !(BitConverter.IsLittleEndian ^ (order == Endianness.Little));
}
}

View File

@@ -0,0 +1,143 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
namespace EmbedIO.Net.Internal
{
internal class RequestStream : Stream
{
private readonly Stream _stream;
private readonly byte[] _buffer;
private int _offset;
private int _length;
private long _remainingBody;
internal RequestStream(Stream stream, byte[] buffer, int offset, int length, long contentLength = -1)
{
_stream = stream;
_buffer = buffer;
_offset = offset;
_length = length;
_remainingBody = contentLength;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush()
{
}
public override int Read([In, Out] byte[] buffer, int offset, int count)
{
// Call FillFromBuffer to check for buffer boundaries even when remaining_body is 0
var nread = FillFromBuffer(buffer, offset, count);
if (nread == -1)
{
// No more bytes available (Content-Length)
return 0;
}
if (nread > 0)
{
return nread;
}
nread = _stream.Read(buffer, offset, count);
if (nread > 0 && _remainingBody > 0)
{
_remainingBody -= nread;
}
return nread;
}
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
// Returns 0 if we can keep reading from the base stream,
// > 0 if we read something from the buffer.
// -1 if we had a content length set and we finished reading that many bytes.
private int FillFromBuffer(byte[] buffer, int off, int count)
{
if (buffer == null)
{
throw new ArgumentNullException(nameof(buffer));
}
if (off < 0)
{
throw new ArgumentOutOfRangeException(nameof(off), "< 0");
}
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), "< 0");
}
var len = buffer.Length;
if (off > len)
{
throw new ArgumentException("destination offset is beyond array size");
}
if (off > len - count)
{
throw new ArgumentException("Reading would overrun buffer");
}
if (_remainingBody == 0)
{
return -1;
}
if (_length == 0)
{
return 0;
}
var size = Math.Min(_length, count);
if (_remainingBody > 0)
{
size = (int) Math.Min(size, _remainingBody);
}
if (_offset > _buffer.Length - size)
{
size = Math.Min(size, _buffer.Length - _offset);
}
if (size == 0)
{
return 0;
}
Buffer.BlockCopy(_buffer, _offset, buffer, off, size);
_offset += size;
_length -= size;
if (_remainingBody > 0)
{
_remainingBody -= size;
}
return size;
}
}
}

View File

@@ -0,0 +1,190 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace EmbedIO.Net.Internal
{
internal class ResponseStream : Stream
{
private static readonly byte[] CrLf = { 13, 10 };
private readonly object _headersSyncRoot = new ();
private readonly Stream _stream;
private readonly HttpListenerResponse _response;
private readonly bool _ignoreErrors;
private bool _disposed;
private bool _trailerSent;
internal ResponseStream(Stream stream, HttpListenerResponse response, bool ignoreErrors)
{
_response = response;
_ignoreErrors = ignoreErrors;
_stream = stream;
}
/// <inheritdoc />
public override bool CanRead => false;
/// <inheritdoc />
public override bool CanSeek => false;
/// <inheritdoc />
public override bool CanWrite => true;
/// <inheritdoc />
public override long Length => throw new NotSupportedException();
/// <inheritdoc />
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
/// <inheritdoc />
public override void Flush()
{
}
/// <inheritdoc />
public override void Write(byte[] buffer, int offset, int count)
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ResponseStream));
}
byte[] bytes;
var ms = GetHeaders(false);
var chunked = _response.SendChunked;
if (ms != null)
{
var start = ms.Position; // After the possible preamble for the encoding
ms.Position = ms.Length;
if (chunked)
{
bytes = GetChunkSizeBytes(count, false);
ms.Write(bytes, 0, bytes.Length);
}
var newCount = Math.Min(count, 16384 - (int)ms.Position + (int)start);
ms.Write(buffer, offset, newCount);
count -= newCount;
offset += newCount;
InternalWrite(ms.ToArray(), (int)start, (int)(ms.Length - start));
ms.SetLength(0);
ms.Capacity = 0; // 'dispose' the buffer in ms.
}
else if (chunked)
{
bytes = GetChunkSizeBytes(count, false);
InternalWrite(bytes, 0, bytes.Length);
}
if (count > 0)
{
InternalWrite(buffer, offset, count);
}
if (chunked)
{
InternalWrite(CrLf, 0, 2);
}
}
/// <inheritdoc />
public override int Read([In, Out] byte[] buffer, int offset, int count) => throw new NotSupportedException();
/// <inheritdoc />
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
/// <inheritdoc />
public override void SetLength(long value) => throw new NotSupportedException();
internal void InternalWrite(byte[] buffer, int offset, int count)
{
if (_ignoreErrors)
{
try
{
_stream.Write(buffer, offset, count);
}
catch
{
// ignored
}
}
else
{
_stream.Write(buffer, offset, count);
}
}
protected override void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (!disposing)
{
return;
}
using var ms = GetHeaders(true);
var chunked = _response.SendChunked;
if (_stream.CanWrite)
{
try
{
byte[] bytes;
if (ms != null)
{
var start = ms.Position;
if (chunked && !_trailerSent)
{
bytes = GetChunkSizeBytes(0, true);
ms.Position = ms.Length;
ms.Write(bytes, 0, bytes.Length);
}
InternalWrite(ms.ToArray(), (int)start, (int)(ms.Length - start));
_trailerSent = true;
}
else if (chunked && !_trailerSent)
{
bytes = GetChunkSizeBytes(0, true);
InternalWrite(bytes, 0, bytes.Length);
_trailerSent = true;
}
}
catch (ObjectDisposedException)
{
// Ignored
}
catch (IOException)
{
// Ignore error due to connection reset by peer
}
}
_response.Close();
}
private static byte[] GetChunkSizeBytes(int size, bool final) => WebServer.DefaultEncoding.GetBytes($"{size:x}\r\n{(final ? "\r\n" : string.Empty)}");
private MemoryStream? GetHeaders(bool closing)
{
lock (_headersSyncRoot)
{
return _response.HeadersSent ? null : _response.SendHeaders(closing);
}
}
}
}

View File

@@ -0,0 +1,77 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace EmbedIO.Net.Internal
{
internal static class StringExtensions
{
private const string TokenSpecialChars = "()<>@,;:\\\"/[]?={} \t";
internal static bool IsToken(this string @this)
=> @this.All(c => c >= 0x20 && c < 0x7f && TokenSpecialChars.IndexOf(c) < 0);
internal static IEnumerable<string> SplitHeaderValue(this string @this, bool useCookieSeparators)
{
var len = @this.Length;
var buff = new StringBuilder(32);
var escaped = false;
var quoted = false;
for (var i = 0; i < len; i++)
{
var c = @this[i];
if (c == '"')
{
if (escaped)
{
escaped = false;
}
else
{
quoted = !quoted;
}
}
else if (c == '\\')
{
if (i < len - 1 && @this[i + 1] == '"')
{
escaped = true;
}
}
else if (c == ',' || (useCookieSeparators && c == ';'))
{
if (!quoted)
{
yield return buff.ToString();
buff.Length = 0;
continue;
}
}
_ = buff.Append(c);
}
if (buff.Length > 0)
{
yield return buff.ToString();
}
}
internal static string Unquote(this string str)
{
var start = str.IndexOf('\"');
var end = str.LastIndexOf('\"');
if (start >= 0 && end >= 0)
{
str = str.Substring(start + 1, end - 1);
}
return str.Trim();
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents a wrapper for <c>System.Net.CookieCollection</c>.
/// </summary>
/// <seealso cref="ICookieCollection" />
internal sealed class SystemCookieCollection : ICookieCollection
{
private readonly CookieCollection _collection;
/// <summary>
/// Initializes a new instance of the <see cref="SystemCookieCollection"/> class.
/// </summary>
/// <param name="collection">The cookie collection.</param>
public SystemCookieCollection(CookieCollection collection)
{
_collection = collection;
}
/// <inheritdoc />
public int Count => _collection.Count;
/// <inheritdoc />
public bool IsSynchronized => _collection.IsSynchronized;
/// <inheritdoc />
public object SyncRoot => _collection.SyncRoot;
/// <inheritdoc />
public Cookie? this[string name] => _collection[name];
/// <inheritdoc />
IEnumerator<Cookie> IEnumerable<Cookie>.GetEnumerator() => _collection.OfType<Cookie>().GetEnumerator();
/// <inheritdoc />
public IEnumerator GetEnumerator() => _collection.GetEnumerator();
/// <inheritdoc />
public void CopyTo(Array array, int index) => _collection.CopyTo(array, index);
/// <inheritdoc />
public void CopyTo(Cookie[] array, int index) => _collection.CopyTo(array, index);
/// <inheritdoc />
public void Add(Cookie cookie) => _collection.Add(cookie);
/// <inheritdoc />
public bool Contains(Cookie cookie) => _collection.OfType<Cookie>().Contains(cookie);
}
}

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using EmbedIO.Authentication;
using EmbedIO.Internal;
using EmbedIO.Routing;
using EmbedIO.Sessions;
using EmbedIO.Utilities;
using EmbedIO.WebSockets;
using EmbedIO.WebSockets.Internal;
using Swan.Logging;
namespace EmbedIO.Net.Internal
{
internal sealed class SystemHttpContext : IHttpContextImpl
{
private readonly System.Net.HttpListenerContext _context;
private readonly TimeKeeper _ageKeeper = new ();
private readonly Stack<Action<IHttpContext>> _closeCallbacks = new ();
private bool _closed;
public SystemHttpContext(System.Net.HttpListenerContext context)
{
_context = context;
Request = new SystemHttpRequest(_context);
User = _context.User ?? Auth.NoUser;
Response = new SystemHttpResponse(_context);
Id = UniqueIdGenerator.GetNext();
LocalEndPoint = Request.LocalEndPoint;
RemoteEndPoint = Request.RemoteEndPoint;
Route = RouteMatch.None;
Session = SessionProxy.None;
}
public string Id { get; }
public CancellationToken CancellationToken { get; set; }
public long Age => _ageKeeper.ElapsedTime;
public IPEndPoint LocalEndPoint { get; }
public IPEndPoint RemoteEndPoint { get; }
public IHttpRequest Request { get; }
public RouteMatch Route { get; set; }
public string RequestedPath => Route.SubPath ?? string.Empty; // It will never be empty, because modules are matched via base routes - this is just to silence a warning.
public IHttpResponse Response { get; }
public IPrincipal User { get; set; }
public ISessionProxy Session { get; set; }
public bool SupportCompressedRequests { get; set; }
public IDictionary<object, object> Items { get; } = new Dictionary<object, object>();
public bool IsHandled { get; private set; }
public MimeTypeProviderStack MimeTypeProviders { get; } = new MimeTypeProviderStack();
public void SetHandled() => IsHandled = true;
public void OnClose(Action<IHttpContext> callback)
{
if (_closed)
{
throw new InvalidOperationException("HTTP context has already been closed.");
}
_closeCallbacks.Push(Validate.NotNull(nameof(callback), callback));
}
public async Task<IWebSocketContext> AcceptWebSocketAsync(
IEnumerable<string> requestedProtocols,
string acceptedProtocol,
int receiveBufferSize,
TimeSpan keepAliveInterval,
CancellationToken cancellationToken)
{
var context = await _context.AcceptWebSocketAsync(
acceptedProtocol.NullIfEmpty(), // Empty string would throw; use null to signify "no subprotocol" here.
receiveBufferSize,
keepAliveInterval)
.ConfigureAwait(false);
return new WebSocketContext(this, context.SecWebSocketVersion, requestedProtocols, acceptedProtocol, new SystemWebSocket(context.WebSocket), cancellationToken);
}
public void Close()
{
_closed = true;
// Always close the response stream no matter what.
Response.Close();
foreach (var callback in _closeCallbacks)
{
try
{
callback(this);
}
catch (Exception e)
{
e.Log("HTTP context", "[Id] Exception thrown by a HTTP context close callback.");
}
}
}
public string GetMimeType(string extension)
=> MimeTypeProviders.GetMimeType(extension);
public bool TryDetermineCompression(string mimeType, out bool preferCompression)
=> MimeTypeProviders.TryDetermineCompression(mimeType, out preferCompression);
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents a wrapper for Microsoft HTTP Listener.
/// </summary>
internal class SystemHttpListener : IHttpListener
{
private readonly System.Net.HttpListener _httpListener;
public SystemHttpListener(System.Net.HttpListener httpListener)
{
_httpListener = httpListener;
}
/// <inheritdoc />
public bool IgnoreWriteExceptions
{
get => _httpListener.IgnoreWriteExceptions;
set => _httpListener.IgnoreWriteExceptions = value;
}
/// <inheritdoc />
public List<string> Prefixes => _httpListener.Prefixes.ToList();
/// <inheritdoc />
public bool IsListening => _httpListener.IsListening;
/// <inheritdoc />
public string Name { get; } = "Microsoft HTTP Listener";
/// <inheritdoc />
public void Start() => _httpListener.Start();
/// <inheritdoc />
public void Stop() => _httpListener.Stop();
/// <inheritdoc />
public void AddPrefix(string urlPrefix) => _httpListener.Prefixes.Add(urlPrefix);
/// <inheritdoc />
public async Task<IHttpContextImpl> GetContextAsync(CancellationToken cancellationToken)
{
// System.Net.HttpListener.GetContextAsync may throw ObjectDisposedException
// when stopping a WebServer. This has been observed on Mono 5.20.1.19
// on Raspberry Pi, but the fact remains that the method does not take
// a CancellationToken as parameter, and WebServerBase<>.RunAsync counts on it.
System.Net.HttpListenerContext context;
try
{
context = await _httpListener.GetContextAsync().ConfigureAwait(false);
}
catch (Exception e) when (cancellationToken.IsCancellationRequested)
{
throw new OperationCanceledException(
"Probable cancellation detected by catching an exception in System.Net.HttpListener.GetContextAsync",
e,
cancellationToken);
}
return new SystemHttpContext(context);
}
void IDisposable.Dispose() => ((IDisposable)_httpListener)?.Dispose();
}
}

View File

@@ -0,0 +1,121 @@
using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Text;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents a wrapper for HttpListenerContext.Request.
/// </summary>
/// <seealso cref="EmbedIO.IHttpRequest" />
public class SystemHttpRequest : IHttpRequest
{
private readonly System.Net.HttpListenerRequest _request;
/// <summary>
/// Initializes a new instance of the <see cref="SystemHttpRequest"/> class.
/// </summary>
/// <param name="context">The context.</param>
public SystemHttpRequest(System.Net.HttpListenerContext context)
{
_request = context.Request;
_ = Enum.TryParse<HttpVerbs>(_request.HttpMethod.Trim(), true, out var verb);
HttpVerb = verb;
Cookies = new SystemCookieCollection(_request.Cookies);
LocalEndPoint = _request.LocalEndPoint!;
RemoteEndPoint = _request.RemoteEndPoint!;
}
/// <inheritdoc />
public NameValueCollection Headers => _request.Headers;
/// <inheritdoc />
public Version ProtocolVersion => _request.ProtocolVersion;
/// <inheritdoc />
public bool KeepAlive => _request.KeepAlive;
/// <inheritdoc />
public ICookieCollection Cookies { get; }
/// <inheritdoc />
public string RawUrl => _request.RawUrl;
/// <inheritdoc />
public NameValueCollection QueryString => _request.QueryString;
/// <inheritdoc />
public string HttpMethod => _request.HttpMethod;
/// <inheritdoc />
public HttpVerbs HttpVerb { get; }
/// <inheritdoc />
public Uri Url => _request.Url;
/// <inheritdoc />
public bool HasEntityBody => _request.HasEntityBody;
/// <inheritdoc />
public Stream InputStream => _request.InputStream;
/// <inheritdoc />
public Encoding ContentEncoding
{
get
{
if (!_request.HasEntityBody || _request.ContentType == null)
{
return WebServer.DefaultEncoding;
}
var charSet = HeaderUtility.GetCharset(ContentType);
if (string.IsNullOrEmpty(charSet))
{
return WebServer.DefaultEncoding;
}
try
{
return Encoding.GetEncoding(charSet);
}
catch (ArgumentException)
{
return WebServer.DefaultEncoding;
}
}
}
/// <inheritdoc />
public IPEndPoint RemoteEndPoint { get; }
/// <inheritdoc />
public bool IsSecureConnection => _request.IsSecureConnection;
/// <inheritdoc />
public bool IsLocal => _request.IsLocal;
/// <inheritdoc />
public string UserAgent => _request.UserAgent;
/// <inheritdoc />
public bool IsWebSocketRequest => _request.IsWebSocketRequest;
/// <inheritdoc />
public IPEndPoint LocalEndPoint { get; }
/// <inheritdoc />
public string ContentType => _request.ContentType;
/// <inheritdoc />
public long ContentLength64 => _request.ContentLength64;
/// <inheritdoc />
public bool IsAuthenticated => _request.IsAuthenticated;
/// <inheritdoc />
public Uri? UrlReferrer => _request.UrlReferrer;
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.IO;
using System.Net;
using System.Text;
namespace EmbedIO.Net.Internal
{
/// <summary>
/// Represents a wrapper for HttpListenerContext.Response.
/// </summary>
/// <seealso cref="IHttpResponse" />
public class SystemHttpResponse : IHttpResponse
{
private readonly System.Net.HttpListenerResponse _response;
/// <summary>
/// Initializes a new instance of the <see cref="SystemHttpResponse"/> class.
/// </summary>
/// <param name="context">The context.</param>
public SystemHttpResponse(System.Net.HttpListenerContext context)
{
_response = context.Response;
Cookies = new SystemCookieCollection(_response.Cookies);
}
/// <inheritdoc />
public WebHeaderCollection Headers => _response.Headers;
/// <inheritdoc />
public int StatusCode
{
get => _response.StatusCode;
set => _response.StatusCode = value;
}
/// <inheritdoc />
public long ContentLength64
{
get => _response.ContentLength64;
set => _response.ContentLength64 = value;
}
/// <inheritdoc />
public string ContentType
{
get => _response.ContentType;
set => _response.ContentType = value;
}
/// <inheritdoc />
public Stream OutputStream => _response.OutputStream;
/// <inheritdoc />
public ICookieCollection Cookies { get; }
/// <inheritdoc />
public Encoding? ContentEncoding
{
get => _response.ContentEncoding;
set => _response.ContentEncoding = value;
}
/// <inheritdoc />
public bool KeepAlive
{
get => _response.KeepAlive;
set => _response.KeepAlive = value;
}
/// <inheritdoc />
public bool SendChunked
{
get => _response.SendChunked;
set => _response.SendChunked = value;
}
/// <inheritdoc />
public Version ProtocolVersion
{
get => _response.ProtocolVersion;
set => _response.ProtocolVersion = value;
}
/// <inheritdoc />
public string StatusDescription
{
get => _response.StatusDescription;
set => _response.StatusDescription = value;
}
/// <inheritdoc />
public void SetCookie(Cookie cookie) => _response.SetCookie(cookie);
/// <inheritdoc />
public void Close() => _response.OutputStream?.Dispose();
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Collections.Specialized;
using System.Globalization;
using System.Net;
using System.Text;
namespace EmbedIO.Net.Internal
{
internal class WebSocketHandshakeResponse
{
private const int HandshakeStatusCode = (int)HttpStatusCode.SwitchingProtocols;
internal WebSocketHandshakeResponse(IHttpContext context)
{
ProtocolVersion = HttpVersion.Version11;
Headers = context.Response.Headers;
Headers.Clear(); // Use only headers mentioned in RFC6455 - scrap all the rest.
StatusCode = HandshakeStatusCode;
Reason = HttpListenerResponseHelper.GetStatusDescription(HandshakeStatusCode);
Headers[HttpHeaderNames.Upgrade] = "websocket";
Headers[HttpHeaderNames.Connection] = "Upgrade";
foreach (var cookie in context.Request.Cookies)
{
Headers.Add("Set-Cookie", cookie.ToString());
}
}
public string Reason { get; }
public int StatusCode { get; }
public NameValueCollection Headers { get; }
public Version ProtocolVersion { get; }
public override string ToString()
{
var output = new StringBuilder(64)
.AppendFormat(CultureInfo.InvariantCulture, "HTTP/{0} {1} {2}\r\n", ProtocolVersion, StatusCode, Reason);
foreach (var key in Headers.AllKeys)
{
_ = output.AppendFormat(CultureInfo.InvariantCulture, "{0}: {1}\r\n", key, Headers[key]);
}
_ = output.Append("\r\n");
return output.ToString();
}
}
}