mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
Permissions, RateControl and Auditing
This commit is contained in:
@@ -42,55 +42,87 @@ using Esiur.Net.Packets.Http;
|
||||
namespace Esiur.Net.Http;
|
||||
public class HttpConnection : NetworkConnection
|
||||
{
|
||||
const long InvalidPacket = long.MinValue;
|
||||
const string WebSocketMagicString = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
const string GenericInternalServerError = "An internal server error occurred.";
|
||||
|
||||
|
||||
MemoryStream websocketFragmentBuffer = new MemoryStream();
|
||||
WebsocketPacket.WSOpcode? websocketFragmentOpcode;
|
||||
ulong websocketFragmentLength;
|
||||
bool websocketCloseSent;
|
||||
uint parsedHttpPacketLength;
|
||||
|
||||
public bool WSMode { get; internal set; }
|
||||
public HttpServer Server { get; internal set; }
|
||||
|
||||
public WebsocketPacket WSRequest { get; set; }
|
||||
public string WebSocketSubprotocol { get; private set; }
|
||||
public HttpRequestPacket Request { get; set; }
|
||||
public HttpResponsePacket Response { get; } = new HttpResponsePacket();
|
||||
|
||||
HttpSession session;
|
||||
|
||||
public HttpSession Session => session;
|
||||
|
||||
public KeyList<string, object> Variables { get; } = new KeyList<string, object>();
|
||||
|
||||
|
||||
|
||||
internal long Parse(byte[] data)
|
||||
{
|
||||
if (WSMode)
|
||||
parsedHttpPacketLength = 0;
|
||||
try
|
||||
{
|
||||
// now parse WS protocol
|
||||
WebsocketPacket ws = new WebsocketPacket();
|
||||
|
||||
var pSize = ws.Parse(data, 0, (uint)data.Length);
|
||||
|
||||
|
||||
if (pSize > 0)
|
||||
if (WSMode)
|
||||
{
|
||||
WSRequest = ws;
|
||||
return 0;
|
||||
var ws = new WebsocketPacket
|
||||
{
|
||||
ExpectedMask = true,
|
||||
MaximumPayloadLength = Server?.MaximumWebSocketMessageLength
|
||||
?? WebsocketPacket.DefaultMaximumPayloadLength
|
||||
};
|
||||
var packetSize = ws.Parse(data, 0, (uint)data.Length);
|
||||
|
||||
if (packetSize > 0)
|
||||
{
|
||||
WSRequest = ws;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return packetSize == 0 ? InvalidPacket : packetSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
return pSize;
|
||||
var request = new HttpRequestPacket();
|
||||
if (Server != null)
|
||||
{
|
||||
request.MaximumContentLength = Server.MaxPost;
|
||||
request.MaximumHeaderLength = Server.MaximumHeaderLength;
|
||||
request.MaximumHeaderCount = Server.MaximumHeaderCount;
|
||||
request.MaximumFormFields = Server.MaximumFormFields;
|
||||
request.MaximumFormKeyLength = Server.MaximumFormKeyLength;
|
||||
request.MaximumFormValueLength = Server.MaximumFormValueLength;
|
||||
request.MaximumMultipartPartLength = Server.MaximumMultipartPartLength;
|
||||
}
|
||||
|
||||
var packetSize = request.Parse(data, 0, (uint)data.Length);
|
||||
if (packetSize > 0)
|
||||
{
|
||||
Request = request;
|
||||
parsedHttpPacketLength = (uint)packetSize;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return packetSize == 0 ? InvalidPacket : packetSize;
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception exception) when (
|
||||
exception is InvalidDataException ||
|
||||
exception is ParserLimitException ||
|
||||
exception is ArgumentException)
|
||||
{
|
||||
var rp = new HttpRequestPacket();
|
||||
var pSize = rp.Parse(data, 0, (uint)data.Length);
|
||||
if (pSize > 0)
|
||||
{
|
||||
Request = rp;
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return pSize;
|
||||
}
|
||||
Global.Log(exception);
|
||||
return InvalidPacket;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,16 +130,26 @@ public class HttpConnection : NetworkConnection
|
||||
public void Flush()
|
||||
{
|
||||
// close the connection
|
||||
if (Request.Headers["connection"].ToLower() != "keep-alive" & IsConnected)
|
||||
if (!string.Equals(
|
||||
Request?.Headers?["connection"],
|
||||
"keep-alive",
|
||||
StringComparison.OrdinalIgnoreCase) && IsConnected)
|
||||
Close();
|
||||
}
|
||||
|
||||
public bool Upgrade()
|
||||
{
|
||||
var ok = Upgrade(Request, Response);
|
||||
var ok = Upgrade(
|
||||
Request,
|
||||
Response,
|
||||
Server?.WebSocketSubprotocols,
|
||||
out var selectedSubprotocol);
|
||||
|
||||
if (ok)
|
||||
{
|
||||
WebSocketSubprotocol = selectedSubprotocol;
|
||||
websocketCloseSent = false;
|
||||
ResetWebSocketFragment();
|
||||
WSMode = true;
|
||||
Send();
|
||||
}
|
||||
@@ -117,28 +159,44 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
public static bool Upgrade(HttpRequestPacket request, HttpResponsePacket response)
|
||||
{
|
||||
if (IsWebsocketRequest(request))
|
||||
{
|
||||
string magicString = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
string ret = request.Headers["Sec-WebSocket-Key"] + magicString;
|
||||
// Compute the SHA1 hash
|
||||
SHA1 sha = SHA1.Create();
|
||||
byte[] sha1Hash = sha.ComputeHash(Encoding.UTF8.GetBytes(ret));
|
||||
response.Headers["Upgrade"] = request.Headers["Upgrade"];
|
||||
response.Headers["Connection"] = request.Headers["Connection"];// "Upgrade";
|
||||
response.Headers["Sec-WebSocket-Accept"] = Convert.ToBase64String(sha1Hash);
|
||||
return Upgrade(request, response, null, out _);
|
||||
}
|
||||
|
||||
if (request.Headers.ContainsKey("Sec-WebSocket-Protocol"))
|
||||
response.Headers["Sec-WebSocket-Protocol"] = request.Headers["Sec-WebSocket-Protocol"];
|
||||
/// <summary>
|
||||
/// Validates a WebSocket handshake and selects at most one mutually supported
|
||||
/// subprotocol. Subprotocol names are case-sensitive as required by RFC 6455.
|
||||
/// </summary>
|
||||
public static bool Upgrade(
|
||||
HttpRequestPacket request,
|
||||
HttpResponsePacket response,
|
||||
IEnumerable<string> supportedSubprotocols,
|
||||
out string selectedSubprotocol)
|
||||
{
|
||||
selectedSubprotocol = null;
|
||||
response?.Headers.RemoveAll("Sec-WebSocket-Protocol");
|
||||
|
||||
if (response == null ||
|
||||
!TryValidateWebSocketRequest(request, out var requestedSubprotocols))
|
||||
return false;
|
||||
|
||||
response.Number = HttpResponseCode.Switching;
|
||||
response.Text = "Switching Protocols";
|
||||
selectedSubprotocol = SelectSubprotocol(
|
||||
requestedSubprotocols,
|
||||
supportedSubprotocols);
|
||||
|
||||
return true;
|
||||
}
|
||||
var challenge = request.Headers["Sec-WebSocket-Key"] + WebSocketMagicString;
|
||||
byte[] sha1Hash;
|
||||
using (var sha = SHA1.Create())
|
||||
sha1Hash = sha.ComputeHash(Encoding.ASCII.GetBytes(challenge));
|
||||
|
||||
return false;
|
||||
response.Headers["Upgrade"] = "websocket";
|
||||
response.Headers["Connection"] = "Upgrade";
|
||||
response.Headers["Sec-WebSocket-Accept"] = Convert.ToBase64String(sha1Hash);
|
||||
if (selectedSubprotocol != null)
|
||||
response.Headers["Sec-WebSocket-Protocol"] = selectedSubprotocol;
|
||||
|
||||
response.Number = HttpResponseCode.Switching;
|
||||
response.Text = "Switching Protocols";
|
||||
return true;
|
||||
}
|
||||
|
||||
public HttpServer Parent
|
||||
@@ -151,8 +209,22 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
public void Send(WebsocketPacket packet)
|
||||
{
|
||||
if (packet.Data != null)
|
||||
if (packet == null)
|
||||
return;
|
||||
|
||||
// This class is always the server side of the built-in WebSocket path.
|
||||
// Recompose even prebuilt packets so caller-supplied masked data cannot be sent.
|
||||
packet.Mask = false;
|
||||
packet.MaximumPayloadLength = IsControlOpcode(packet.Opcode)
|
||||
? 125
|
||||
: Server?.MaximumWebSocketMessageLength
|
||||
?? WebsocketPacket.DefaultMaximumPayloadLength;
|
||||
if (packet.Compose())
|
||||
{
|
||||
if (packet.Opcode == WebsocketPacket.WSOpcode.ConnectionClose)
|
||||
websocketCloseSent = true;
|
||||
base.Send(packet.Data);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Send(string data)
|
||||
@@ -215,6 +287,8 @@ public class HttpConnection : NetworkConnection
|
||||
cookie.Expires = DateTime.MaxValue;
|
||||
cookie.Path = "/";
|
||||
cookie.HttpOnly = true;
|
||||
cookie.Secure = Server.SSL;
|
||||
cookie.SameSite = HttpCookieSameSite.Lax;
|
||||
|
||||
Response.Cookies.Add(cookie);
|
||||
}
|
||||
@@ -228,33 +302,224 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
public static bool IsWebsocketRequest(HttpRequestPacket request)
|
||||
{
|
||||
if (request.Headers.ContainsKey("connection")
|
||||
&& request.Headers["connection"].ToLower().Contains("upgrade")
|
||||
&& request.Headers.ContainsKey("upgrade")
|
||||
&& request.Headers["upgrade"].ToLower() == "websocket"
|
||||
&& request.Headers.ContainsKey("Sec-WebSocket-Version")
|
||||
&& request.Headers["Sec-WebSocket-Version"] == "13"
|
||||
&& request.Headers.ContainsKey("Sec-WebSocket-Key"))
|
||||
//&& Request.Headers.ContainsKey("Sec-WebSocket-Protocol"))
|
||||
return TryValidateWebSocketRequest(request, out _);
|
||||
}
|
||||
|
||||
private static bool TryValidateWebSocketRequest(
|
||||
HttpRequestPacket request,
|
||||
out string[] requestedSubprotocols)
|
||||
{
|
||||
requestedSubprotocols = Array.Empty<string>();
|
||||
if (request == null ||
|
||||
request.Headers == null ||
|
||||
request.Method != Packets.Http.HttpMethod.GET ||
|
||||
(request.RawMethod != null &&
|
||||
!string.Equals(request.RawMethod, "GET", StringComparison.Ordinal)) ||
|
||||
!string.Equals(request.Version, "HTTP/1.1", StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
if (!TryParseTokenList(request.Headers["Connection"], out var connectionTokens) ||
|
||||
!ContainsToken(connectionTokens, "Upgrade", StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!TryParseUpgradeList(request.Headers["Upgrade"], out var hasWebSocket) ||
|
||||
!hasWebSocket)
|
||||
return false;
|
||||
|
||||
if (!string.Equals(
|
||||
request.Headers["Sec-WebSocket-Version"],
|
||||
"13",
|
||||
StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
var key = request.Headers["Sec-WebSocket-Key"];
|
||||
if (!IsCanonicalWebSocketKey(key))
|
||||
return false;
|
||||
|
||||
var protocols = request.Headers["Sec-WebSocket-Protocol"];
|
||||
if (protocols != null && !TryParseTokenList(protocols, out requestedSubprotocols))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsWebSocketUpgradeAttempt(HttpRequestPacket request)
|
||||
{
|
||||
var upgrade = request?.Headers?["Upgrade"];
|
||||
if (string.IsNullOrEmpty(upgrade))
|
||||
return false;
|
||||
|
||||
for (var index = 0; index < upgrade.Length;)
|
||||
{
|
||||
return true;
|
||||
while (index < upgrade.Length && !IsHttpTokenCharacter(upgrade[index]))
|
||||
index++;
|
||||
var start = index;
|
||||
while (index < upgrade.Length && IsHttpTokenCharacter(upgrade[index]))
|
||||
index++;
|
||||
|
||||
if (index > start &&
|
||||
string.Equals(
|
||||
upgrade.Substring(start, index - start),
|
||||
"websocket",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
else
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsCanonicalWebSocketKey(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var decoded = Convert.FromBase64String(key);
|
||||
return decoded.Length == 16 &&
|
||||
string.Equals(
|
||||
Convert.ToBase64String(decoded),
|
||||
key,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseTokenList(string value, out string[] tokens)
|
||||
{
|
||||
tokens = Array.Empty<string>();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return false;
|
||||
|
||||
var parts = value.Split(',');
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
parts[i] = parts[i].Trim();
|
||||
if (!IsHttpToken(parts[i]))
|
||||
return false;
|
||||
}
|
||||
|
||||
tokens = parts;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseUpgradeList(string value, out bool hasWebSocket)
|
||||
{
|
||||
hasWebSocket = false;
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return false;
|
||||
|
||||
foreach (var entry in value.Split(','))
|
||||
{
|
||||
var protocol = entry.Trim();
|
||||
var slash = protocol.IndexOf('/');
|
||||
if (slash < 0)
|
||||
{
|
||||
if (!IsHttpToken(protocol))
|
||||
return false;
|
||||
|
||||
if (string.Equals(protocol, "websocket", StringComparison.OrdinalIgnoreCase))
|
||||
hasWebSocket = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (slash == 0 ||
|
||||
slash == protocol.Length - 1 ||
|
||||
protocol.IndexOf('/', slash + 1) >= 0 ||
|
||||
!IsHttpToken(protocol.Substring(0, slash)) ||
|
||||
!IsHttpToken(protocol.Substring(slash + 1)))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsHttpToken(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return false;
|
||||
|
||||
foreach (var character in value)
|
||||
{
|
||||
if (IsHttpTokenCharacter(character))
|
||||
continue;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsHttpTokenCharacter(char character)
|
||||
=> (character >= 'a' && character <= 'z') ||
|
||||
(character >= 'A' && character <= 'Z') ||
|
||||
(character >= '0' && character <= '9') ||
|
||||
character == '!' || character == '#' || character == '$' ||
|
||||
character == '%' || character == '&' || character == '\'' ||
|
||||
character == '*' || character == '+' || character == '-' ||
|
||||
character == '.' || character == '^' || character == '_' ||
|
||||
character == '`' || character == '|' || character == '~';
|
||||
|
||||
private static bool ContainsToken(
|
||||
string[] tokens,
|
||||
string expected,
|
||||
StringComparison comparison)
|
||||
{
|
||||
foreach (var token in tokens)
|
||||
if (string.Equals(token, expected, comparison))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string SelectSubprotocol(
|
||||
string[] requestedSubprotocols,
|
||||
IEnumerable<string> supportedSubprotocols)
|
||||
{
|
||||
if (supportedSubprotocols == null)
|
||||
return null;
|
||||
|
||||
foreach (var supported in supportedSubprotocols)
|
||||
{
|
||||
if (!IsHttpToken(supported))
|
||||
continue;
|
||||
|
||||
if (ContainsToken(
|
||||
requestedSubprotocols,
|
||||
supported,
|
||||
StringComparison.Ordinal))
|
||||
return supported;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override void DataReceived(NetworkBuffer data)
|
||||
{
|
||||
if (WSMode)
|
||||
{
|
||||
ProcessWebSocketData(data);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] msg = data.Read();
|
||||
if (msg == null)
|
||||
return;
|
||||
|
||||
var BL = Parse(msg);
|
||||
|
||||
if (BL == 0)
|
||||
if (BL == InvalidPacket)
|
||||
{
|
||||
if (Request.Method == Packets.Http.HttpMethod.UNKNOWN)
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
else if (BL == 0)
|
||||
{
|
||||
if (Request == null || Request.Method == Packets.Http.HttpMethod.UNKNOWN)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
@@ -272,47 +537,42 @@ public class HttpConnection : NetworkConnection
|
||||
}
|
||||
else if (BL < 0)
|
||||
{
|
||||
data.HoldFor(msg, (uint)(msg.Length - BL));
|
||||
return;
|
||||
}
|
||||
else if (BL > 0)
|
||||
{
|
||||
if (BL > Server.MaxPost)
|
||||
var requiredLength = (ulong)msg.Length + (ulong)(-BL);
|
||||
if (requiredLength > uint.MaxValue)
|
||||
{
|
||||
Send(
|
||||
"<html><body>POST method content is larger than "
|
||||
+ Server.MaxPost
|
||||
+ " bytes.</body></html>");
|
||||
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
else
|
||||
|
||||
data.HoldFor(msg, (uint)requiredLength);
|
||||
return;
|
||||
}
|
||||
|
||||
RestoreSessionFromRequest();
|
||||
|
||||
if (!WSMode && IsWebSocketUpgradeAttempt(Request))
|
||||
{
|
||||
if (!IsWebsocketRequest(Request) || !Upgrade())
|
||||
{
|
||||
data.HoldFor(msg, (uint)(msg.Length + BL));
|
||||
Response.Number = HttpResponseCode.BadRequest;
|
||||
Response.Text = "Bad Request";
|
||||
Response.Headers["Connection"] = "close";
|
||||
Send("Invalid WebSocket handshake.");
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
else if (BL < 0) // for security
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (IsWebsocketRequest(Request) & !WSMode)
|
||||
{
|
||||
Upgrade();
|
||||
//return;
|
||||
}
|
||||
|
||||
|
||||
//return;
|
||||
|
||||
try
|
||||
{
|
||||
if (!Server.Execute(this))
|
||||
if (Server == null || !Server.Execute(this))
|
||||
{
|
||||
if (WSMode)
|
||||
{
|
||||
FailWebSocket(1008, "No HTTP filter accepted the WebSocket connection.");
|
||||
return;
|
||||
}
|
||||
|
||||
Response.Number = HttpResponseCode.InternalServerError;
|
||||
Send("Bad Request");
|
||||
Close();
|
||||
@@ -325,23 +585,360 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
Global.Log("HTTPServer", LogType.Error, ex.ToString());
|
||||
|
||||
//Console.WriteLine(ex.ToString());
|
||||
//EventLog.WriteEntry("HttpServer", ex.ToString(), EventLogEntryType.Error);
|
||||
Send(Error500(ex.Message));
|
||||
if (WSMode)
|
||||
{
|
||||
FailWebSocket(1011, "A WebSocket filter failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
Response.Number = HttpResponseCode.InternalServerError;
|
||||
Response.Headers["Content-Type"] = "text/html; charset=utf-8";
|
||||
Send(FormatError500Page(ex));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (WSMode &&
|
||||
IsConnected &&
|
||||
parsedHttpPacketLength > 0 &&
|
||||
parsedHttpPacketLength < msg.Length)
|
||||
{
|
||||
data.Write(
|
||||
msg,
|
||||
parsedHttpPacketLength,
|
||||
(uint)msg.Length - parsedHttpPacketLength);
|
||||
ProcessWebSocketData(data);
|
||||
}
|
||||
}
|
||||
|
||||
private string Error500(string msg)
|
||||
internal void RestoreSessionFromRequest()
|
||||
{
|
||||
session = null;
|
||||
var sessionId = Request?.Cookies?["SID"];
|
||||
if (Server?.TryGetSession(sessionId, out var restored) != true)
|
||||
return;
|
||||
|
||||
session = restored;
|
||||
session.Refresh();
|
||||
}
|
||||
|
||||
private void ProcessWebSocketData(NetworkBuffer data)
|
||||
{
|
||||
var message = data.Read();
|
||||
if (message == null)
|
||||
return;
|
||||
|
||||
var offset = 0u;
|
||||
var ends = (uint)message.Length;
|
||||
|
||||
while (offset < ends && IsConnected)
|
||||
{
|
||||
var packet = new WebsocketPacket
|
||||
{
|
||||
ExpectedMask = true,
|
||||
MaximumPayloadLength = GetIncomingFrameLimit(message[offset])
|
||||
};
|
||||
|
||||
long packetLength;
|
||||
try
|
||||
{
|
||||
packetLength = packet.Parse(message, offset, ends);
|
||||
}
|
||||
catch (ParserLimitException exception)
|
||||
{
|
||||
FailWebSocket(1009, exception.Message);
|
||||
return;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is InvalidDataException ||
|
||||
exception is ArgumentException)
|
||||
{
|
||||
FailWebSocket(
|
||||
IsInvalidUtf8(exception) ? (ushort)1007 : (ushort)1002,
|
||||
exception.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (packetLength < 0)
|
||||
{
|
||||
var remaining = ends - offset;
|
||||
var required = (ulong)remaining + (ulong)(-packetLength);
|
||||
if (required > int.MaxValue)
|
||||
{
|
||||
FailWebSocket(1009, "The incomplete WebSocket frame is too large to buffer.");
|
||||
return;
|
||||
}
|
||||
|
||||
data.HoldFor(message, offset, remaining, (uint)required);
|
||||
return;
|
||||
}
|
||||
|
||||
if (packetLength == 0 || (ulong)packetLength > ends - offset)
|
||||
{
|
||||
FailWebSocket(1002, "The WebSocket frame parser returned an invalid length.");
|
||||
return;
|
||||
}
|
||||
|
||||
offset += (uint)packetLength;
|
||||
if (!ProcessWebSocketFrame(packet))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private ulong GetIncomingFrameLimit(byte firstHeaderByte)
|
||||
{
|
||||
var opcode = (WebsocketPacket.WSOpcode)(firstHeaderByte & 0x0F);
|
||||
if (IsControlOpcode(opcode))
|
||||
return 125;
|
||||
|
||||
return Server?.MaximumWebSocketMessageLength
|
||||
?? WebsocketPacket.DefaultMaximumPayloadLength;
|
||||
}
|
||||
|
||||
private static bool IsControlOpcode(WebsocketPacket.WSOpcode opcode)
|
||||
=> opcode == WebsocketPacket.WSOpcode.ConnectionClose ||
|
||||
opcode == WebsocketPacket.WSOpcode.Ping ||
|
||||
opcode == WebsocketPacket.WSOpcode.Pong;
|
||||
|
||||
private bool ProcessWebSocketFrame(WebsocketPacket packet)
|
||||
{
|
||||
switch (packet.Opcode)
|
||||
{
|
||||
case WebsocketPacket.WSOpcode.Ping:
|
||||
SendWebSocketFrame(WebsocketPacket.WSOpcode.Pong, packet.Message);
|
||||
return IsConnected;
|
||||
|
||||
case WebsocketPacket.WSOpcode.Pong:
|
||||
return true;
|
||||
|
||||
case WebsocketPacket.WSOpcode.ConnectionClose:
|
||||
ResetWebSocketFragment();
|
||||
if (!websocketCloseSent)
|
||||
{
|
||||
websocketCloseSent = true;
|
||||
SendWebSocketCloseAndClose(packet.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Close();
|
||||
}
|
||||
return false;
|
||||
|
||||
case WebsocketPacket.WSOpcode.TextFrame:
|
||||
case WebsocketPacket.WSOpcode.BinaryFrame:
|
||||
if (websocketFragmentOpcode.HasValue)
|
||||
{
|
||||
FailWebSocket(
|
||||
1002,
|
||||
"A new WebSocket data frame arrived before the fragmented message completed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (packet.FIN)
|
||||
return DeliverWebSocketMessage(packet);
|
||||
|
||||
websocketFragmentOpcode = packet.Opcode;
|
||||
websocketFragmentLength = 0;
|
||||
websocketFragmentBuffer.SetLength(0);
|
||||
return AppendWebSocketFragment(packet.Message);
|
||||
|
||||
case WebsocketPacket.WSOpcode.ContinuationFrame:
|
||||
if (!websocketFragmentOpcode.HasValue)
|
||||
{
|
||||
FailWebSocket(
|
||||
1002,
|
||||
"A WebSocket continuation frame arrived without an active fragmented message.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AppendWebSocketFragment(packet.Message))
|
||||
return false;
|
||||
|
||||
if (!packet.FIN)
|
||||
return true;
|
||||
|
||||
var opcode = websocketFragmentOpcode.Value;
|
||||
var completeMessage = websocketFragmentBuffer.ToArray();
|
||||
ResetWebSocketFragment();
|
||||
|
||||
try
|
||||
{
|
||||
if (opcode == WebsocketPacket.WSOpcode.TextFrame)
|
||||
WebsocketPacket.ValidateTextPayload(completeMessage);
|
||||
}
|
||||
catch (InvalidDataException exception)
|
||||
{
|
||||
FailWebSocket(1007, exception.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
return DeliverWebSocketMessage(new WebsocketPacket
|
||||
{
|
||||
FIN = true,
|
||||
Opcode = opcode,
|
||||
Mask = true,
|
||||
Message = completeMessage,
|
||||
PayloadLength = completeMessage.LongLength
|
||||
});
|
||||
|
||||
default:
|
||||
FailWebSocket(1002, "Unsupported WebSocket opcode.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool AppendWebSocketFragment(byte[] payload)
|
||||
{
|
||||
payload ??= Array.Empty<byte>();
|
||||
var payloadLength = (ulong)payload.LongLength;
|
||||
if (payloadLength > ulong.MaxValue - websocketFragmentLength)
|
||||
{
|
||||
FailWebSocket(1009, "The fragmented WebSocket message length overflowed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var nextLength = websocketFragmentLength + payloadLength;
|
||||
var maximumLength = Server?.MaximumWebSocketMessageLength
|
||||
?? WebsocketPacket.DefaultMaximumPayloadLength;
|
||||
if (nextLength > int.MaxValue ||
|
||||
(maximumLength > 0 && nextLength > maximumLength))
|
||||
{
|
||||
FailWebSocket(
|
||||
1009,
|
||||
$"The fragmented WebSocket message exceeds the {maximumLength}-byte limit.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.Length > 0)
|
||||
websocketFragmentBuffer.Write(payload, 0, payload.Length);
|
||||
websocketFragmentLength = nextLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool DeliverWebSocketMessage(WebsocketPacket packet)
|
||||
{
|
||||
WSRequest = packet;
|
||||
try
|
||||
{
|
||||
if (Server == null)
|
||||
{
|
||||
FailWebSocket(1011, "The WebSocket connection is no longer assigned to a server.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Server.Execute(this))
|
||||
{
|
||||
FailWebSocket(1008, "No HTTP filter accepted the WebSocket message.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsConnected;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Global.Log("HTTPServer", LogType.Error, exception.ToString());
|
||||
FailWebSocket(1011, "A WebSocket filter failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SendWebSocketFrame(
|
||||
WebsocketPacket.WSOpcode opcode,
|
||||
byte[] payload)
|
||||
{
|
||||
var packet = new WebsocketPacket
|
||||
{
|
||||
FIN = true,
|
||||
Mask = false,
|
||||
Opcode = opcode,
|
||||
Message = payload ?? Array.Empty<byte>(),
|
||||
MaximumPayloadLength = 0
|
||||
};
|
||||
|
||||
if (packet.Compose())
|
||||
base.Send(packet.Data);
|
||||
}
|
||||
|
||||
private void FailWebSocket(ushort closeCode, string message)
|
||||
{
|
||||
Global.Log("HTTPServer", LogType.Warning, message);
|
||||
ResetWebSocketFragment();
|
||||
|
||||
if (IsConnected && !websocketCloseSent)
|
||||
{
|
||||
websocketCloseSent = true;
|
||||
SendWebSocketCloseAndClose(
|
||||
new[] { (byte)(closeCode >> 8), (byte)closeCode });
|
||||
return;
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
private void SendWebSocketCloseAndClose(byte[] payload)
|
||||
{
|
||||
var packet = new WebsocketPacket
|
||||
{
|
||||
FIN = true,
|
||||
Mask = false,
|
||||
Opcode = WebsocketPacket.WSOpcode.ConnectionClose,
|
||||
Message = payload ?? Array.Empty<byte>(),
|
||||
MaximumPayloadLength = 125
|
||||
};
|
||||
|
||||
if (!packet.Compose())
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
base.SendAsync(packet.Data, 0, packet.Data.Length)
|
||||
.Then(_ => Close())
|
||||
.Error(_ => Close());
|
||||
}
|
||||
|
||||
private void ResetWebSocketFragment()
|
||||
{
|
||||
websocketFragmentOpcode = null;
|
||||
websocketFragmentLength = 0;
|
||||
if (websocketFragmentBuffer.Capacity > 64 * 1024)
|
||||
{
|
||||
websocketFragmentBuffer.Dispose();
|
||||
websocketFragmentBuffer = new MemoryStream();
|
||||
}
|
||||
else
|
||||
{
|
||||
websocketFragmentBuffer.SetLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsInvalidUtf8(Exception exception)
|
||||
{
|
||||
for (var current = exception; current != null; current = current.InnerException)
|
||||
if (current is DecoderFallbackException)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static string FormatError500Page(string msg)
|
||||
{
|
||||
var encodedMessage = WebUtility.HtmlEncode(msg ?? string.Empty);
|
||||
return "<html><head><title>500 Internal Server Error</title></head><br>\r\n"
|
||||
+ "<body><br>\r\n"
|
||||
+ "<b>500</b> Internal Server Error<br>" + msg + "\r\n"
|
||||
+ "<b>500</b> Internal Server Error<br>" + encodedMessage + "\r\n"
|
||||
+ "</body><br>\r\n"
|
||||
+ "</html><br>\r\n";
|
||||
}
|
||||
|
||||
internal string FormatError500Page(Exception exception)
|
||||
{
|
||||
var message = Server?.ExposeExceptionDetails == true
|
||||
? exception?.Message
|
||||
: GenericInternalServerError;
|
||||
return FormatError500Page(message);
|
||||
}
|
||||
|
||||
public async AsyncReply<bool> SendFile(string filename)
|
||||
{
|
||||
if (Response.Handled == true)
|
||||
@@ -411,7 +1008,8 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
while (true)
|
||||
{
|
||||
var n = fs.Read(buffer, 0, 60000);
|
||||
var n = await fs.ReadAsync(buffer, 0, buffer.Length)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (n <= 0)
|
||||
break;
|
||||
@@ -447,6 +1045,6 @@ public class HttpConnection : NetworkConnection
|
||||
|
||||
protected override void Disconnected()
|
||||
{
|
||||
// do nothing
|
||||
ResetWebSocketFragment();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,11 +41,13 @@ using System.Text.RegularExpressions;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Esiur.Net.Packets.Http;
|
||||
using Esiur.Net.Packets.WebSocket;
|
||||
|
||||
namespace Esiur.Net.Http;
|
||||
public class HttpServer : NetworkServer<HttpConnection>, IResource
|
||||
{
|
||||
Dictionary<string, HttpSession> sessions = new Dictionary<string, HttpSession>();
|
||||
readonly object sessionsLock = new object();
|
||||
HttpFilter[] filters = new HttpFilter[0];
|
||||
|
||||
Dictionary<Packets.Http.HttpMethod, List<RouteInfo>> routes = new()
|
||||
@@ -156,6 +158,72 @@ public class HttpServer : NetworkServer<HttpConnection>, IResource
|
||||
|
||||
//[Attribute]
|
||||
public virtual uint MaxPost
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumContentLength;
|
||||
|
||||
public virtual uint MaximumHeaderLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumHeaderLength;
|
||||
|
||||
public virtual int MaximumHeaderCount
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumHeaderCount;
|
||||
|
||||
public virtual int MaximumFormFields
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumFormFields;
|
||||
|
||||
public virtual int MaximumFormKeyLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumFormKeyLength;
|
||||
|
||||
public virtual int MaximumFormValueLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumFormValueLength;
|
||||
|
||||
public virtual int MaximumMultipartPartLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = HttpPacketHelpers.DefaultMaximumMultipartPartLength;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum payload accumulated for one WebSocket application message, including
|
||||
/// all of its fragments. Set to zero to disable the configured limit.
|
||||
/// </summary>
|
||||
public virtual ulong MaximumWebSocketMessageLength
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = WebsocketPacket.DefaultMaximumPayloadLength;
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket subprotocols supported by this server, in server preference order.
|
||||
/// A protocol is returned to the client only when it was also requested.
|
||||
/// </summary>
|
||||
public virtual string[] WebSocketSubprotocols
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether HTTP 500 responses may include exception messages. Disabled by default
|
||||
/// to avoid disclosing implementation details to remote clients.
|
||||
/// </summary>
|
||||
public virtual bool ExposeExceptionDetails
|
||||
{
|
||||
get;
|
||||
set;
|
||||
@@ -179,39 +247,99 @@ public class HttpServer : NetworkServer<HttpConnection>, IResource
|
||||
public HttpSession CreateSession(string id, int timeout)
|
||||
{
|
||||
var s = new HttpSession();
|
||||
s.OnEnd += SessionEnded;
|
||||
s.OnDestroy += SessionDestroyed;
|
||||
|
||||
s.Set(id, timeout);
|
||||
lock (sessionsLock)
|
||||
sessions.Add(id, s);
|
||||
|
||||
|
||||
sessions.Add(id, s);
|
||||
try
|
||||
{
|
||||
s.Set(id, timeout);
|
||||
}
|
||||
catch
|
||||
{
|
||||
lock (sessionsLock)
|
||||
sessions.Remove(id);
|
||||
s.Destroy();
|
||||
throw;
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
public static string MakeCookie(string Item, string Value, DateTime Expires, string Domain, string Path, bool HttpOnly)
|
||||
/// <summary>
|
||||
/// Looks up a live HTTP session by its cookie identifier.
|
||||
/// </summary>
|
||||
public bool TryGetSession(string id, out HttpSession session)
|
||||
{
|
||||
session = null;
|
||||
if (string.IsNullOrEmpty(id))
|
||||
return false;
|
||||
|
||||
//Set-Cookie: ckGeneric=CookieBody; expires=Sun, 30-Dec-2001 21:00:00 GMT; domain=.com.au; path=/
|
||||
//Set-Cookie: SessionID=another; expires=Fri, 29 Jun 2006 20:47:11 UTC; path=/
|
||||
string Cookie = Item + "=" + Value;
|
||||
lock (sessionsLock)
|
||||
{
|
||||
if (!sessions.TryGetValue(id, out var candidate) || candidate.IsDestroyed)
|
||||
return false;
|
||||
|
||||
if (Expires.Ticks != 0)
|
||||
{
|
||||
Cookie += "; expires=" + Expires.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss") + " GMT";
|
||||
session = candidate;
|
||||
return true;
|
||||
}
|
||||
if (Domain != null)
|
||||
}
|
||||
|
||||
private void SessionEnded(HttpSession session)
|
||||
{
|
||||
RemoveSession(session);
|
||||
session.Destroy();
|
||||
}
|
||||
|
||||
private void SessionDestroyed(object sender)
|
||||
{
|
||||
if (sender is HttpSession session)
|
||||
RemoveSession(session);
|
||||
}
|
||||
|
||||
private void RemoveSession(HttpSession session)
|
||||
{
|
||||
lock (sessionsLock)
|
||||
{
|
||||
Cookie += "; domain=" + Domain;
|
||||
if (session.Id != null &&
|
||||
sessions.TryGetValue(session.Id, out var current) &&
|
||||
ReferenceEquals(current, session))
|
||||
sessions.Remove(session.Id);
|
||||
}
|
||||
if (Path != null)
|
||||
}
|
||||
|
||||
public static string MakeCookie(string Item, string Value, DateTime Expires, string Domain, string Path, bool HttpOnly)
|
||||
=> MakeCookie(
|
||||
Item,
|
||||
Value,
|
||||
Expires,
|
||||
Domain,
|
||||
Path,
|
||||
HttpOnly,
|
||||
false,
|
||||
HttpCookieSameSite.Unspecified);
|
||||
|
||||
public static string MakeCookie(
|
||||
string Item,
|
||||
string Value,
|
||||
DateTime Expires,
|
||||
string Domain,
|
||||
string Path,
|
||||
bool HttpOnly,
|
||||
bool Secure,
|
||||
HttpCookieSameSite SameSite)
|
||||
{
|
||||
return new HttpCookie(Item, Value)
|
||||
{
|
||||
Cookie += "; path=" + Path;
|
||||
}
|
||||
if (HttpOnly)
|
||||
{
|
||||
Cookie += "; HttpOnly";
|
||||
}
|
||||
return Cookie;
|
||||
Expires = Expires,
|
||||
Domain = Domain,
|
||||
Path = Path,
|
||||
HttpOnly = HttpOnly,
|
||||
Secure = Secure,
|
||||
SameSite = SameSite,
|
||||
}.ToString();
|
||||
}
|
||||
|
||||
protected override void ClientDisconnected(HttpConnection connection)
|
||||
@@ -321,6 +449,7 @@ public class HttpServer : NetworkServer<HttpConnection>, IResource
|
||||
else if (operation == ResourceOperation.Terminate)
|
||||
{
|
||||
Stop();
|
||||
DisposeSessions();
|
||||
}
|
||||
else if (operation == ResourceOperation.SystemReloading)
|
||||
{
|
||||
@@ -336,6 +465,19 @@ public class HttpServer : NetworkServer<HttpConnection>, IResource
|
||||
|
||||
}
|
||||
|
||||
private void DisposeSessions()
|
||||
{
|
||||
HttpSession[] activeSessions;
|
||||
lock (sessionsLock)
|
||||
{
|
||||
activeSessions = sessions.Values.ToArray();
|
||||
sessions.Clear();
|
||||
}
|
||||
|
||||
foreach (var activeSession in activeSessions)
|
||||
activeSession.Destroy();
|
||||
}
|
||||
|
||||
|
||||
public override void Add(HttpConnection connection)
|
||||
{
|
||||
|
||||
@@ -44,6 +44,9 @@ public class HttpSession : IDestructible //<T> where T : TClient
|
||||
private string id;
|
||||
private Timer timer;
|
||||
private int timeout;
|
||||
private readonly object timerLock = new object();
|
||||
private long timerGeneration;
|
||||
private bool destroyed;
|
||||
DateTime creation;
|
||||
DateTime lastAction;
|
||||
|
||||
@@ -63,25 +66,44 @@ public class HttpSession : IDestructible //<T> where T : TClient
|
||||
variables = new KeyList<string, object>();
|
||||
variables.OnModified += new KeyList<string, object>.Modified(VariablesModified);
|
||||
creation = DateTime.Now;
|
||||
lastAction = creation;
|
||||
}
|
||||
|
||||
internal void Set(string id, int timeout)
|
||||
{
|
||||
//modified = sessionModifiedEvent;
|
||||
//ended = sessionEndEvent;
|
||||
this.id = id;
|
||||
if (timeout < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(timeout));
|
||||
|
||||
if (this.timeout != 0)
|
||||
lock (timerLock)
|
||||
{
|
||||
if (destroyed)
|
||||
throw new ObjectDisposedException(nameof(HttpSession));
|
||||
|
||||
this.id = id;
|
||||
this.timeout = timeout;
|
||||
timer = new Timer(OnSessionEndTimerCallback, null, TimeSpan.FromSeconds(timeout), TimeSpan.FromSeconds(0));
|
||||
creation = DateTime.Now;
|
||||
lastAction = creation;
|
||||
ScheduleTimerLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSessionEndTimerCallback(object o)
|
||||
{
|
||||
OnEnd?.Invoke(this);
|
||||
SessionEndedEvent onEnd;
|
||||
|
||||
lock (timerLock)
|
||||
{
|
||||
if (destroyed || !(o is long generation) || generation != timerGeneration)
|
||||
return;
|
||||
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
onEnd = OnEnd;
|
||||
}
|
||||
|
||||
onEnd?.Invoke(this);
|
||||
}
|
||||
|
||||
void VariablesModified(string key, object oldValue, object newValue, KeyList<string, object> sender)
|
||||
@@ -91,15 +113,54 @@ public class HttpSession : IDestructible //<T> where T : TClient
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
OnDestroy?.Invoke(this);
|
||||
timer.Dispose();
|
||||
timer = null;
|
||||
DestroyedEvent onDestroy;
|
||||
|
||||
lock (timerLock)
|
||||
{
|
||||
if (destroyed)
|
||||
return;
|
||||
|
||||
destroyed = true;
|
||||
timerGeneration++;
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
variables.OnModified -= VariablesModified;
|
||||
onDestroy = OnDestroy;
|
||||
OnDestroy = null;
|
||||
OnEnd = null;
|
||||
OnModify = null;
|
||||
}
|
||||
|
||||
onDestroy?.Invoke(this);
|
||||
}
|
||||
|
||||
internal void Refresh()
|
||||
{
|
||||
lastAction = DateTime.Now;
|
||||
timer.Change(TimeSpan.FromSeconds(timeout), TimeSpan.FromSeconds(0));
|
||||
lock (timerLock)
|
||||
{
|
||||
if (destroyed)
|
||||
return;
|
||||
|
||||
lastAction = DateTime.Now;
|
||||
ScheduleTimerLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleTimerLocked()
|
||||
{
|
||||
timerGeneration++;
|
||||
timer?.Dispose();
|
||||
timer = null;
|
||||
|
||||
if (timeout <= 0)
|
||||
return;
|
||||
|
||||
var generation = timerGeneration;
|
||||
timer = new Timer(
|
||||
OnSessionEndTimerCallback,
|
||||
generation,
|
||||
TimeSpan.FromSeconds(timeout),
|
||||
System.Threading.Timeout.InfiniteTimeSpan);
|
||||
}
|
||||
|
||||
public int Timeout // Seconds
|
||||
@@ -110,8 +171,18 @@ public class HttpSession : IDestructible //<T> where T : TClient
|
||||
}
|
||||
set
|
||||
{
|
||||
timeout = value;
|
||||
Refresh();
|
||||
if (value < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
lock (timerLock)
|
||||
{
|
||||
if (destroyed)
|
||||
return;
|
||||
|
||||
timeout = value;
|
||||
lastAction = DateTime.Now;
|
||||
ScheduleTimerLocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,5 +195,14 @@ public class HttpSession : IDestructible //<T> where T : TClient
|
||||
{
|
||||
get { return lastAction; }
|
||||
}
|
||||
|
||||
internal bool IsDestroyed
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (timerLock)
|
||||
return destroyed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user