mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-31 01:40:42 +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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
|
||||
Copyright (c) 2017 Ahmed Kh. Zamil
|
||||
|
||||
@@ -23,31 +23,25 @@ SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Esiur.Data;
|
||||
using Esiur.Misc;
|
||||
|
||||
namespace Esiur.Net;
|
||||
|
||||
public class NetworkBuffer
|
||||
{
|
||||
byte[] data;
|
||||
private static readonly byte[] Empty = Array.Empty<byte>();
|
||||
|
||||
uint neededDataLength = 0;
|
||||
|
||||
object syncLock = new object();
|
||||
|
||||
public NetworkBuffer()
|
||||
{
|
||||
data = new byte[0];
|
||||
}
|
||||
private readonly object syncLock = new object();
|
||||
private byte[] buffer = Empty;
|
||||
private int start;
|
||||
private int length;
|
||||
private uint neededDataLength;
|
||||
|
||||
public bool Protected
|
||||
{
|
||||
get
|
||||
{
|
||||
return neededDataLength > data.Length;
|
||||
lock (syncLock)
|
||||
return (uint)length < neededDataLength;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,76 +49,88 @@ public class NetworkBuffer
|
||||
{
|
||||
get
|
||||
{
|
||||
return (uint)data.Length;
|
||||
lock (syncLock)
|
||||
return (uint)length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void HoldForNextWrite(byte[] src)
|
||||
{
|
||||
HoldFor(src, (uint)src.Length + 1);
|
||||
if (src == null)
|
||||
throw new ArgumentNullException(nameof(src));
|
||||
|
||||
HoldFor(src, 0, (uint)src.Length, CheckedNextLength((uint)src.Length));
|
||||
}
|
||||
|
||||
public void HoldForNextWrite(byte[] src, uint offset, uint size)
|
||||
{
|
||||
HoldFor(src, offset, size, size + 1);
|
||||
}
|
||||
|
||||
=> HoldFor(src, offset, size, CheckedNextLength(size));
|
||||
|
||||
public void HoldFor(byte[] src, uint offset, uint size, uint needed)
|
||||
{
|
||||
ValidateRange(src, offset, size);
|
||||
ValidateNeededLength(needed);
|
||||
|
||||
if (size >= needed)
|
||||
// Preserve the historical exception contract for this semantic error.
|
||||
throw new Exception("Size >= Needed !");
|
||||
|
||||
lock (syncLock)
|
||||
{
|
||||
if (size >= needed)
|
||||
throw new Exception("Size >= Needed !");
|
||||
|
||||
//trim = true;
|
||||
data = DC.Combine(src, offset, size, data, 0, (uint)data.Length);
|
||||
Prepend(src, (int)offset, (int)size);
|
||||
neededDataLength = needed;
|
||||
}
|
||||
}
|
||||
|
||||
public void HoldFor(byte[] src, uint needed)
|
||||
{
|
||||
if (src == null)
|
||||
throw new ArgumentNullException(nameof(src));
|
||||
|
||||
HoldFor(src, 0, (uint)src.Length, needed);
|
||||
}
|
||||
|
||||
public bool Protect(byte[] data, uint offset, uint needed)
|
||||
{
|
||||
uint dataLength = (uint)data.Length - offset;
|
||||
if (data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
if (offset > (uint)data.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
|
||||
// protection
|
||||
if (dataLength < needed)
|
||||
{
|
||||
HoldFor(data, offset, dataLength, needed);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
ValidateNeededLength(needed);
|
||||
|
||||
var dataLength = (uint)data.Length - offset;
|
||||
if (dataLength >= needed)
|
||||
return false;
|
||||
|
||||
// HoldFor validates that dataLength is strictly less than needed.
|
||||
HoldFor(data, offset, dataLength, needed);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Write(byte[] src)
|
||||
{
|
||||
if (src == null)
|
||||
throw new ArgumentNullException(nameof(src));
|
||||
|
||||
Write(src, 0, (uint)src.Length);
|
||||
}
|
||||
|
||||
public void Write(byte[] src, uint offset, uint length)
|
||||
{
|
||||
ValidateRange(src, offset, length);
|
||||
if (length == 0)
|
||||
return;
|
||||
|
||||
lock (syncLock)
|
||||
DC.Append(ref data, src, offset, length);
|
||||
Append(src, (int)offset, (int)length);
|
||||
}
|
||||
|
||||
public bool CanRead
|
||||
{
|
||||
get
|
||||
{
|
||||
if (data.Length == 0)
|
||||
return false;
|
||||
if (data.Length < neededDataLength)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
lock (syncLock)
|
||||
return length != 0 && (uint)length >= neededDataLength;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,34 +138,176 @@ public class NetworkBuffer
|
||||
{
|
||||
lock (syncLock)
|
||||
{
|
||||
if (data.Length == 0)
|
||||
if (length == 0 || (uint)length < neededDataLength)
|
||||
return null;
|
||||
|
||||
byte[] rt = null;
|
||||
byte[] result;
|
||||
|
||||
if (neededDataLength == 0)
|
||||
// A single write, and geometrically grown power-of-two payloads, can transfer
|
||||
// ownership without one final copy. Otherwise return an exact-size array, as the
|
||||
// historical API did, and release the working buffer.
|
||||
if (start == 0 && length == buffer.Length)
|
||||
{
|
||||
rt = data;
|
||||
data = new byte[0];
|
||||
return rt;
|
||||
result = buffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if (data.Length >= neededDataLength)
|
||||
{
|
||||
rt = data;
|
||||
data = new byte[0];
|
||||
|
||||
neededDataLength = 0;
|
||||
return rt;
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
result = new byte[length];
|
||||
Buffer.BlockCopy(buffer, start, result, 0, length);
|
||||
}
|
||||
|
||||
buffer = Empty;
|
||||
start = 0;
|
||||
length = 0;
|
||||
neededDataLength = 0;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private void Append(byte[] src, int offset, int count)
|
||||
{
|
||||
if (length == 0 && buffer.Length == 0)
|
||||
{
|
||||
// The overwhelmingly common path is one socket read followed by one Read().
|
||||
// Allocate exactly once so Read can transfer this array without copying it.
|
||||
buffer = new byte[count];
|
||||
Buffer.BlockCopy(src, offset, buffer, 0, count);
|
||||
start = 0;
|
||||
length = count;
|
||||
return;
|
||||
}
|
||||
|
||||
var requiredLength = CheckedCombinedLength(length, count);
|
||||
var writeOffset = start + length;
|
||||
|
||||
if (buffer.Length - writeOffset < count)
|
||||
{
|
||||
if (requiredLength <= buffer.Length)
|
||||
{
|
||||
// Reuse headroom left by a prepend operation.
|
||||
Buffer.BlockCopy(buffer, start, buffer, 0, length);
|
||||
start = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Grow(requiredLength, prependLength: 0);
|
||||
}
|
||||
|
||||
writeOffset = start + length;
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(src, offset, buffer, writeOffset, count);
|
||||
length = requiredLength;
|
||||
}
|
||||
|
||||
private void Prepend(byte[] src, int offset, int count)
|
||||
{
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
if (length == 0 && buffer.Length == 0)
|
||||
{
|
||||
buffer = new byte[count];
|
||||
Buffer.BlockCopy(src, offset, buffer, 0, count);
|
||||
start = 0;
|
||||
length = count;
|
||||
return;
|
||||
}
|
||||
|
||||
var requiredLength = CheckedCombinedLength(length, count);
|
||||
|
||||
if (start >= count)
|
||||
{
|
||||
start -= count;
|
||||
}
|
||||
else if (requiredLength <= buffer.Length)
|
||||
{
|
||||
// Re-center the live region once and leave any remaining spare capacity around it.
|
||||
var combinedStart = (buffer.Length - requiredLength) / 2;
|
||||
Buffer.BlockCopy(buffer, start, buffer, combinedStart + count, length);
|
||||
start = combinedStart;
|
||||
}
|
||||
else
|
||||
{
|
||||
Grow(requiredLength, count);
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(src, offset, buffer, start, count);
|
||||
length = requiredLength;
|
||||
}
|
||||
|
||||
private void Grow(int requiredLength, int prependLength)
|
||||
{
|
||||
var newCapacity = GetExpandedCapacity(buffer.Length, requiredLength);
|
||||
var replacement = new byte[newCapacity];
|
||||
|
||||
if (prependLength == 0)
|
||||
{
|
||||
if (length > 0)
|
||||
Buffer.BlockCopy(buffer, start, replacement, 0, length);
|
||||
start = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var combinedStart = (newCapacity - requiredLength) / 2;
|
||||
if (length > 0)
|
||||
Buffer.BlockCopy(buffer, start, replacement, combinedStart + prependLength, length);
|
||||
start = combinedStart;
|
||||
}
|
||||
|
||||
buffer = replacement;
|
||||
}
|
||||
|
||||
private static int GetExpandedCapacity(int currentCapacity, int requiredLength)
|
||||
{
|
||||
var capacity = currentCapacity == 0 ? 256 : currentCapacity;
|
||||
|
||||
while (capacity < requiredLength)
|
||||
{
|
||||
if (capacity > int.MaxValue / 2)
|
||||
return requiredLength;
|
||||
|
||||
capacity *= 2;
|
||||
}
|
||||
|
||||
return capacity;
|
||||
}
|
||||
|
||||
private static int CheckedCombinedLength(int currentLength, int additionalLength)
|
||||
{
|
||||
if (additionalLength > int.MaxValue - currentLength)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(additionalLength),
|
||||
"The buffered data exceeds the maximum managed array length.");
|
||||
|
||||
return currentLength + additionalLength;
|
||||
}
|
||||
|
||||
private static uint CheckedNextLength(uint size)
|
||||
{
|
||||
if (size >= int.MaxValue)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(size),
|
||||
"The requested held length exceeds the maximum managed array length.");
|
||||
|
||||
return size + 1;
|
||||
}
|
||||
|
||||
private static void ValidateNeededLength(uint needed)
|
||||
{
|
||||
if (needed > int.MaxValue)
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(needed),
|
||||
"The requested held length exceeds the maximum managed array length.");
|
||||
}
|
||||
|
||||
private static void ValidateRange(byte[] src, uint offset, uint count)
|
||||
{
|
||||
if (src == null)
|
||||
throw new ArgumentNullException(nameof(src));
|
||||
if (offset > (uint)src.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
if (count > (uint)src.Length - offset)
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ SOFTWARE.
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -39,13 +40,27 @@ namespace Esiur.Net;
|
||||
/// </summary>
|
||||
public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocket>
|
||||
{
|
||||
private ISocket sock;
|
||||
private volatile ISocket sock;
|
||||
private DateTime lastAction;
|
||||
|
||||
// Re-entrancy guard for NetworkReceive. 0 = idle, 1 = a thread is draining the buffer.
|
||||
// Interlocked is used instead of a plain bool so concurrent receive callbacks cannot
|
||||
// both enter the drain loop (which is not safe to run from two threads at once).
|
||||
private int receiving;
|
||||
private readonly object receiveLock = new object();
|
||||
private readonly Queue<PendingReceive> pendingReceives = new Queue<PendingReceive>();
|
||||
private bool receiving;
|
||||
private long socketGeneration;
|
||||
|
||||
private readonly struct PendingReceive
|
||||
{
|
||||
public PendingReceive(ISocket sender, NetworkBuffer buffer, long generation)
|
||||
{
|
||||
Sender = sender;
|
||||
Buffer = buffer;
|
||||
Generation = generation;
|
||||
}
|
||||
|
||||
public ISocket Sender { get; }
|
||||
public NetworkBuffer Buffer { get; }
|
||||
public long Generation { get; }
|
||||
}
|
||||
|
||||
public delegate void NetworkConnectionEvent(NetworkConnection connection);
|
||||
|
||||
@@ -69,9 +84,19 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
||||
|
||||
public virtual void Assign(ISocket socket)
|
||||
{
|
||||
lastAction = DateTime.Now;
|
||||
sock = socket;
|
||||
sock.Receiver = this;
|
||||
lock (receiveLock)
|
||||
{
|
||||
lastAction = DateTime.Now;
|
||||
|
||||
if (!ReferenceEquals(sock, socket))
|
||||
{
|
||||
socketGeneration++;
|
||||
pendingReceives.Clear();
|
||||
}
|
||||
|
||||
sock = socket;
|
||||
sock.Receiver = this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -80,14 +105,19 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
||||
/// </summary>
|
||||
public ISocket Unassign()
|
||||
{
|
||||
if (sock == null)
|
||||
return null;
|
||||
lock (receiveLock)
|
||||
{
|
||||
if (sock == null)
|
||||
return null;
|
||||
|
||||
sock.Receiver = null;
|
||||
sock.Receiver = null;
|
||||
|
||||
var detached = sock;
|
||||
sock = null;
|
||||
return detached;
|
||||
var detached = sock;
|
||||
sock = null;
|
||||
socketGeneration++;
|
||||
pendingReceives.Clear();
|
||||
return detached;
|
||||
}
|
||||
}
|
||||
|
||||
public void Close()
|
||||
@@ -163,12 +193,24 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
||||
|
||||
public void NetworkClose(ISocket socket)
|
||||
{
|
||||
lock (receiveLock)
|
||||
{
|
||||
if (!ReferenceEquals(socket, sock))
|
||||
return;
|
||||
|
||||
pendingReceives.Clear();
|
||||
}
|
||||
|
||||
Disconnected();
|
||||
OnClose?.Invoke(this);
|
||||
}
|
||||
|
||||
public void NetworkConnect(ISocket socket)
|
||||
{
|
||||
lock (receiveLock)
|
||||
if (!ReferenceEquals(socket, sock))
|
||||
return;
|
||||
|
||||
Connected();
|
||||
OnConnect?.Invoke(this);
|
||||
}
|
||||
@@ -181,30 +223,95 @@ public abstract class NetworkConnection : IDestructible, INetworkReceiver<ISocke
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ignore callbacks once the socket is unassigned or closed.
|
||||
if (sock == null || sock.State == SocketState.Closed)
|
||||
return;
|
||||
|
||||
lastAction = DateTime.Now;
|
||||
|
||||
// Only one thread drains the buffer at a time; others return immediately and
|
||||
// rely on the active drainer to pick up the newly appended data.
|
||||
if (Interlocked.CompareExchange(ref receiving, 1, 0) != 0)
|
||||
return;
|
||||
|
||||
try
|
||||
bool drain;
|
||||
lock (receiveLock)
|
||||
{
|
||||
while (buffer.Available > 0 && !buffer.Protected)
|
||||
DataReceived(buffer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref receiving, 0);
|
||||
// A callback can outlive Unassign/Assign. Only the socket that currently
|
||||
// owns this connection may enqueue work for its protocol parser.
|
||||
if (!ReferenceEquals(sender, sock) || sender.State == SocketState.Closed)
|
||||
return;
|
||||
|
||||
lastAction = DateTime.Now;
|
||||
pendingReceives.Enqueue(new PendingReceive(sender, buffer, socketGeneration));
|
||||
|
||||
if (receiving)
|
||||
return;
|
||||
|
||||
receiving = true;
|
||||
drain = true;
|
||||
}
|
||||
|
||||
if (drain)
|
||||
DrainReceiveQueue();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Global.Log("NetworkConnection:NetworkReceive", LogType.Warning, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainReceiveQueue()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
PendingReceive pending = default;
|
||||
var found = false;
|
||||
|
||||
lock (receiveLock)
|
||||
{
|
||||
while (pendingReceives.Count > 0)
|
||||
{
|
||||
var candidate = pendingReceives.Dequeue();
|
||||
if (ReferenceEquals(candidate.Sender, sock)
|
||||
&& candidate.Generation == socketGeneration)
|
||||
{
|
||||
pending = candidate;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
receiving = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
while (IsCurrentReceive(pending)
|
||||
&& pending.Buffer.Available > 0
|
||||
&& !pending.Buffer.Protected)
|
||||
{
|
||||
DataReceived(pending.Buffer);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Keep the queue usable after a protocol parser fails. Any work already
|
||||
// queued for a replacement socket can still be drained safely.
|
||||
Global.Log("NetworkConnection:NetworkReceive", LogType.Warning, ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsCurrentReceive(PendingReceive pending)
|
||||
{
|
||||
lock (receiveLock)
|
||||
{
|
||||
if (!ReferenceEquals(pending.Sender, sock)
|
||||
|| pending.Generation != socketGeneration)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
return pending.Sender.State != SocketState.Closed;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,20 @@ namespace Esiur.Net;
|
||||
|
||||
public abstract class NetworkServer<TConnection> : IDestructible where TConnection : NetworkConnection, new()
|
||||
{
|
||||
private Sockets.ISocket listener;
|
||||
private volatile Sockets.ISocket listener;
|
||||
private readonly object lifecycleLock = new object();
|
||||
public AutoList<TConnection, NetworkServer<TConnection>> Connections { get; internal set; }
|
||||
|
||||
private Thread thread;
|
||||
|
||||
private Timer timer;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time allowed for an accepted socket to finish protocol initialization
|
||||
/// (for example, a TLS handshake). A zero or negative value disables the deadline.
|
||||
/// </summary>
|
||||
public TimeSpan ConnectionInitializationTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
|
||||
public event DestroyedEvent OnDestroy;
|
||||
|
||||
|
||||
@@ -78,68 +85,142 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
|
||||
|
||||
public void Start(Sockets.ISocket socket)//, uint timeout, uint clock)
|
||||
{
|
||||
if (listener != null)
|
||||
return;
|
||||
if (socket == null)
|
||||
throw new ArgumentNullException(nameof(socket));
|
||||
|
||||
|
||||
Connections = new AutoList<TConnection, NetworkServer<TConnection>>(this);
|
||||
|
||||
|
||||
if (Timeout > 0 & Clock > 0)
|
||||
lock (lifecycleLock)
|
||||
{
|
||||
timer = new Timer(MinuteThread, null, TimeSpan.FromMinutes(0), TimeSpan.FromSeconds(Clock));
|
||||
}
|
||||
if (listener != null)
|
||||
return;
|
||||
|
||||
Connections = new AutoList<TConnection, NetworkServer<TConnection>>(this);
|
||||
|
||||
listener = socket;
|
||||
|
||||
thread = new Thread(new ThreadStart(() =>
|
||||
{
|
||||
while (true)
|
||||
if (Timeout > 0 && Clock > 0)
|
||||
{
|
||||
timer = new Timer(MinuteThread, null, TimeSpan.FromMinutes(0), TimeSpan.FromSeconds(Clock));
|
||||
}
|
||||
|
||||
listener = socket;
|
||||
|
||||
// Bind this thread to this particular Start invocation. If the server is
|
||||
// stopped and restarted before the old Accept call unwinds, the old thread
|
||||
// must not begin accepting from the replacement listener.
|
||||
thread = new Thread(() => AcceptLoop(socket))
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void AcceptLoop(ISocket activeListener)
|
||||
{
|
||||
while (ReferenceEquals(listener, activeListener))
|
||||
{
|
||||
try
|
||||
{
|
||||
var acceptedSocket = activeListener.Accept();
|
||||
|
||||
if (acceptedSocket == null)
|
||||
return;
|
||||
|
||||
TConnection connection = null;
|
||||
var stopped = false;
|
||||
|
||||
// Admission and Stop's connection snapshot share this gate. Therefore
|
||||
// either the connection is added before Stop snapshots it, or Stop wins
|
||||
// and this accepted socket is closed without being exposed to the server.
|
||||
lock (lifecycleLock)
|
||||
{
|
||||
if (!ReferenceEquals(listener, activeListener))
|
||||
{
|
||||
stopped = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
connection = new TConnection();
|
||||
connection.Assign(acceptedSocket);
|
||||
Add(connection);
|
||||
stopped = !ReferenceEquals(listener, activeListener);
|
||||
}
|
||||
}
|
||||
|
||||
if (stopped)
|
||||
{
|
||||
try { acceptedSocket.Close(); } catch { }
|
||||
return;
|
||||
}
|
||||
|
||||
// A derived server can reject admission (for example, due to a per-peer
|
||||
// connection quota) by not adding the connection and closing its socket.
|
||||
if (!Connections.Contains(connection))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var s = listener.Accept();
|
||||
|
||||
if (s == null)
|
||||
{
|
||||
//Global.Log("NetworkServer", LogType.Error, "sock == null");
|
||||
return;
|
||||
}
|
||||
|
||||
var c = new TConnection();
|
||||
|
||||
c.Assign(s);
|
||||
Add(c);
|
||||
|
||||
// A derived server can reject admission (for example, due to a per-peer
|
||||
// connection quota) by not adding the connection and closing its socket.
|
||||
if (!Connections.Contains(c))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
ClientConnected(c);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// something wrong with the child.
|
||||
}
|
||||
|
||||
s.Begin();
|
||||
|
||||
ClientConnected(connection);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
Global.Log(ex);
|
||||
// something wrong with the child.
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(listener, activeListener))
|
||||
{
|
||||
try { connection.Close(); } catch { }
|
||||
return;
|
||||
}
|
||||
|
||||
// Some socket implementations perform a protocol handshake in Begin
|
||||
// (notably SSLSocket). Never run that handshake on the single accept
|
||||
// thread: a peer that stops mid-handshake would otherwise prevent all
|
||||
// subsequent clients from being accepted.
|
||||
_ = BeginAcceptedSocketAsync(acceptedSocket);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (!ReferenceEquals(listener, activeListener))
|
||||
return;
|
||||
|
||||
Global.Log(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BeginAcceptedSocketAsync(ISocket socket)
|
||||
{
|
||||
try
|
||||
{
|
||||
var beginTask = AwaitSocketBeginAsync(socket);
|
||||
var timeout = ConnectionInitializationTimeout;
|
||||
|
||||
if (timeout > TimeSpan.Zero)
|
||||
{
|
||||
var completed = await Task.WhenAny(beginTask, Task.Delay(timeout)).ConfigureAwait(false);
|
||||
if (!ReferenceEquals(completed, beginTask))
|
||||
{
|
||||
try { socket.Close(); } catch { }
|
||||
_ = beginTask.ContinueWith(
|
||||
completedTask => _ = completedTask.Exception,
|
||||
TaskContinuationOptions.OnlyOnFaulted);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
thread.Start();
|
||||
|
||||
if (!await beginTask.ConfigureAwait(false))
|
||||
try { socket.Close(); } catch { }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Global.Log("NetworkServer", LogType.Warning,
|
||||
$"Accepted socket initialization failed: {ex.Message}");
|
||||
try { socket.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<bool> AwaitSocketBeginAsync(ISocket socket)
|
||||
=> await socket.BeginAsync();
|
||||
|
||||
|
||||
//[Attribute]
|
||||
public uint Timeout
|
||||
@@ -160,25 +241,39 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
|
||||
public void Stop()
|
||||
{
|
||||
var port = 0;
|
||||
ISocket currentListener = null;
|
||||
TConnection[] connections = null;
|
||||
Timer currentTimer = null;
|
||||
|
||||
try
|
||||
{
|
||||
var currentListener = listener;
|
||||
lock (lifecycleLock)
|
||||
{
|
||||
currentListener = listener;
|
||||
listener = null;
|
||||
connections = Connections?.ToArray();
|
||||
currentTimer = timer;
|
||||
timer = null;
|
||||
}
|
||||
|
||||
if (currentListener != null)
|
||||
{
|
||||
// Reading the endpoint can throw if the socket is already disposed (e.g. a second
|
||||
// Stop or the finalizer after Destroy), so it is best-effort and only used for logging.
|
||||
try { port = currentListener.LocalEndPoint.Port; } catch { }
|
||||
try { currentListener.Close(); } catch { }
|
||||
listener = null; // make Stop idempotent
|
||||
}
|
||||
|
||||
foreach (TConnection con in Connections.ToArray())
|
||||
try { con.Close(); } catch { }
|
||||
if (connections != null)
|
||||
{
|
||||
foreach (TConnection con in connections)
|
||||
try { con.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Global.Log("NetworkServer", LogType.Warning, $"Server@{port} is down.");
|
||||
try { currentTimer?.Dispose(); } catch { }
|
||||
Global.Log("NetworkServer", LogType.Warning, $"Server on port {port} is down.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +295,8 @@ public abstract class NetworkServer<TConnection> : IDestructible where TConnecti
|
||||
{
|
||||
get
|
||||
{
|
||||
return listener.State == SocketState.Listening;
|
||||
var currentListener = listener;
|
||||
return currentListener != null && currentListener.State == SocketState.Listening;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -125,19 +125,6 @@ public class EpAuthPacket : Packet
|
||||
set;
|
||||
}
|
||||
|
||||
private uint dataLengthNeeded;
|
||||
|
||||
bool NotEnough(uint offset, uint ends, uint needed)
|
||||
{
|
||||
if (offset + needed > ends)
|
||||
{
|
||||
dataLengthNeeded = needed - (ends - offset);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Command.ToString() + " " + Method.ToString();
|
||||
@@ -145,12 +132,13 @@ public class EpAuthPacket : Packet
|
||||
|
||||
public override long Parse(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
ValidateBounds(data, offset, ends);
|
||||
Tdu = null;
|
||||
|
||||
var oOffset = offset;
|
||||
|
||||
if (NotEnough(offset, ends, 1))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 1, out var incomplete))
|
||||
return incomplete;
|
||||
|
||||
Command = (EpAuthPacketCommand)(data[offset] >> 6);
|
||||
var hasTdu = (data[offset] & 0x20) != 0;
|
||||
@@ -183,18 +171,21 @@ public class EpAuthPacket : Packet
|
||||
|
||||
if (hasTdu)
|
||||
{
|
||||
if (NotEnough(offset, ends, 1))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 1, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
var maximumPacketSize = _warehouse.Configuration.Parser.MaximumPacketSize;
|
||||
var maximumPayloadLength = maximumPacketSize == 0
|
||||
? (ulong)int.MaxValue
|
||||
: Math.Min(maximumPacketSize, (ulong)int.MaxValue);
|
||||
Tdu = PlainTdu.Parse(
|
||||
data,
|
||||
offset,
|
||||
ends,
|
||||
maximumPacketSize == 0 ? ulong.MaxValue : maximumPacketSize);
|
||||
maximumPayloadLength);
|
||||
|
||||
if (Tdu.Value.Class == TduClass.Invalid)
|
||||
return -(int)Tdu.Value.TotalLength;
|
||||
return -(long)Tdu.Value.TotalLength;
|
||||
|
||||
offset += (uint)Tdu.Value.TotalLength;
|
||||
|
||||
|
||||
@@ -51,9 +51,6 @@ class EpPacket : Packet
|
||||
|
||||
public PlainTdu? Tdu { get; set; }
|
||||
|
||||
private uint dataLengthNeeded;
|
||||
private uint originalOffset;
|
||||
|
||||
Warehouse _warehouse;
|
||||
|
||||
public EpPacket(Warehouse warehouse)
|
||||
@@ -61,11 +58,6 @@ class EpPacket : Packet
|
||||
_warehouse = warehouse;
|
||||
}
|
||||
|
||||
public override bool Compose()
|
||||
{
|
||||
return base.Compose();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Method switch
|
||||
@@ -78,24 +70,13 @@ class EpPacket : Packet
|
||||
};
|
||||
}
|
||||
|
||||
bool NotEnough(uint offset, uint ends, uint needed)
|
||||
{
|
||||
if (offset + needed > ends)
|
||||
{
|
||||
dataLengthNeeded = needed - (ends - offset);
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
public override long Parse(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
originalOffset = offset;
|
||||
ValidateBounds(data, offset, ends);
|
||||
var originalOffset = offset;
|
||||
|
||||
if (NotEnough(offset, ends, 1))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 1, out var incomplete))
|
||||
return incomplete;
|
||||
|
||||
var hasDTU = (data[offset] & 0x20) == 0x20;
|
||||
|
||||
@@ -109,8 +90,8 @@ class EpPacket : Packet
|
||||
{
|
||||
Request = (EpPacketRequest)(data[offset++] & 0x1f);
|
||||
|
||||
if (NotEnough(offset, ends, 4))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 4, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
CallbackId = data.GetUInt32(offset, Endian.Little);
|
||||
offset += 4;
|
||||
@@ -119,8 +100,8 @@ class EpPacket : Packet
|
||||
{
|
||||
Reply = (EpPacketReply)(data[offset++] & 0x1f);
|
||||
|
||||
if (NotEnough(offset, ends, 4))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 4, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
CallbackId = data.GetUInt32(offset, Endian.Little);
|
||||
offset += 4;
|
||||
@@ -132,30 +113,26 @@ class EpPacket : Packet
|
||||
|
||||
if (hasDTU)
|
||||
{
|
||||
if (NotEnough(offset, ends, 1))
|
||||
return -dataLengthNeeded;
|
||||
if (TryGetMissingBytes(offset, ends, 1, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
var maximumPacketSize = _warehouse.Configuration.Parser.MaximumPacketSize;
|
||||
var maximumPayloadLength = maximumPacketSize == 0
|
||||
? (ulong)int.MaxValue
|
||||
: Math.Min(maximumPacketSize, (ulong)int.MaxValue);
|
||||
Tdu = PlainTdu.Parse(
|
||||
data,
|
||||
offset,
|
||||
ends,
|
||||
maximumPacketSize == 0 ? ulong.MaxValue : maximumPacketSize);
|
||||
maximumPayloadLength);
|
||||
|
||||
if (Tdu.Value.Class == TduClass.Invalid)
|
||||
return -(int)Tdu.Value.TotalLength;
|
||||
return -(long)Tdu.Value.TotalLength;
|
||||
|
||||
//Tdu = ParsedTdu.ParseSync(data, offset, ends, _warehouse);
|
||||
|
||||
//if (Tdu.Value.Class == TduClass.Invalid)
|
||||
// return -(int)Tdu.Value.TotalLength;
|
||||
|
||||
//offset += (uint)Tdu.Value.TotalLength;
|
||||
offset += (uint)Tdu.Value.TotalLength;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Tdu = null;
|
||||
Tdu = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
||||
namespace Esiur.Net.Packets.Http
|
||||
{
|
||||
public enum HttpCookieSameSite
|
||||
{
|
||||
Unspecified,
|
||||
Lax,
|
||||
Strict,
|
||||
None,
|
||||
}
|
||||
|
||||
public struct HttpCookie
|
||||
{
|
||||
public string Name;
|
||||
@@ -11,6 +20,8 @@ namespace Esiur.Net.Packets.Http
|
||||
public DateTime Expires;
|
||||
public string Path;
|
||||
public bool HttpOnly;
|
||||
public bool Secure;
|
||||
public HttpCookieSameSite SameSite;
|
||||
public string Domain;
|
||||
|
||||
public HttpCookie(string name, string value)
|
||||
@@ -20,6 +31,8 @@ namespace Esiur.Net.Packets.Http
|
||||
Path = null;
|
||||
Expires = DateTime.MinValue;
|
||||
HttpOnly = false;
|
||||
Secure = false;
|
||||
SameSite = HttpCookieSameSite.Unspecified;
|
||||
Domain = null;
|
||||
}
|
||||
|
||||
@@ -29,6 +42,8 @@ namespace Esiur.Net.Packets.Http
|
||||
Value = value;
|
||||
Expires = expires;
|
||||
HttpOnly = false;
|
||||
Secure = false;
|
||||
SameSite = HttpCookieSameSite.Unspecified;
|
||||
Domain = null;
|
||||
Path = null;
|
||||
}
|
||||
@@ -40,7 +55,7 @@ namespace Esiur.Net.Packets.Http
|
||||
var cookie = Name + "=" + Value;
|
||||
|
||||
if (Expires.Ticks != 0)
|
||||
cookie += "; expires=" + Expires.ToUniversalTime().ToString("ddd, dd MMM yyyy HH:mm:ss") + " GMT";
|
||||
cookie += "; expires=" + Expires.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture);
|
||||
|
||||
if (Domain != null)
|
||||
cookie += "; domain=" + Domain;
|
||||
@@ -51,6 +66,12 @@ namespace Esiur.Net.Packets.Http
|
||||
if (HttpOnly)
|
||||
cookie += "; HttpOnly";
|
||||
|
||||
if (Secure)
|
||||
cookie += "; Secure";
|
||||
|
||||
if (SameSite != HttpCookieSameSite.Unspecified)
|
||||
cookie += "; SameSite=" + SameSite;
|
||||
|
||||
return cookie;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using Esiur.Data;
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace Esiur.Net.Packets.Http;
|
||||
|
||||
internal static class HttpPacketHelpers
|
||||
{
|
||||
internal const uint DefaultMaximumHeaderLength = 64 * 1024;
|
||||
internal const uint DefaultMaximumContentLength = 8 * 1024 * 1024;
|
||||
internal const int DefaultMaximumHeaderCount = 100;
|
||||
internal const int DefaultMaximumFormFields = 1_024;
|
||||
internal const int DefaultMaximumFormKeyLength = 2_048;
|
||||
internal const int DefaultMaximumFormValueLength = 1024 * 1024;
|
||||
internal const int DefaultMaximumMultipartPartLength = 4 * 1024 * 1024;
|
||||
|
||||
internal static bool TryFindHeaderEnd(
|
||||
byte[] data,
|
||||
uint offset,
|
||||
uint ends,
|
||||
uint maximumHeaderLength,
|
||||
out uint bodyOffset)
|
||||
{
|
||||
bodyOffset = 0;
|
||||
var available = ends - offset;
|
||||
var scanLength = maximumHeaderLength == 0
|
||||
? available
|
||||
: available < maximumHeaderLength ? available : maximumHeaderLength;
|
||||
|
||||
if (scanLength >= 4)
|
||||
{
|
||||
var scanEnds = offset + scanLength;
|
||||
for (var i = offset; i <= scanEnds - 4; i++)
|
||||
{
|
||||
if (data[i] == '\r' && data[i + 1] == '\n' &&
|
||||
data[i + 2] == '\r' && data[i + 3] == '\n')
|
||||
{
|
||||
bodyOffset = i + 4;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (maximumHeaderLength > 0 && available >= maximumHeaderLength)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP header exceeds the {maximumHeaderLength}-byte limit.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static string[] ReadHeaderLines(
|
||||
byte[] data,
|
||||
uint offset,
|
||||
uint bodyOffset,
|
||||
int maximumHeaderCount)
|
||||
{
|
||||
var headerContentLength = bodyOffset - offset - 4;
|
||||
var headerEnd = offset + headerContentLength;
|
||||
var headerCount = 0;
|
||||
|
||||
// Count before Split allocates its result so a header made of thousands of
|
||||
// tiny lines is rejected without creating thousands of strings first.
|
||||
for (var i = offset; i + 1 < headerEnd; i++)
|
||||
{
|
||||
if (data[i] != '\r' || data[i + 1] != '\n')
|
||||
continue;
|
||||
|
||||
headerCount++;
|
||||
if (maximumHeaderCount > 0 && headerCount > maximumHeaderCount)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP header count exceeds the {maximumHeaderCount}-header limit.");
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return Encoding.ASCII
|
||||
.GetString(data, (int)offset, (int)headerContentLength)
|
||||
.Split(new[] { "\r\n" }, StringSplitOptions.None);
|
||||
}
|
||||
}
|
||||
@@ -1,303 +1,358 @@
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2017 Ahmed Kh. Zamil
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Esiur.Misc;
|
||||
using Esiur.Data;
|
||||
using Esiur.Misc;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text;
|
||||
|
||||
namespace Esiur.Net.Packets.Http;
|
||||
|
||||
public class HttpRequestPacket : Packet
|
||||
{
|
||||
|
||||
|
||||
public StringKeyList Query;
|
||||
public HttpMethod Method;
|
||||
public string RawMethod;
|
||||
public StringKeyList Headers;
|
||||
|
||||
public bool WSMode;
|
||||
|
||||
public string Version;
|
||||
public StringKeyList Cookies; // String
|
||||
public string URL; /// With query
|
||||
public string Filename; /// Without query
|
||||
|
||||
public StringKeyList Cookies;
|
||||
public string URL;
|
||||
public string Filename;
|
||||
public KeyList<string, object> PostForms;
|
||||
public byte[] Message;
|
||||
|
||||
|
||||
private HttpMethod GetMethod(string method)
|
||||
{
|
||||
switch (method.ToLower())
|
||||
{
|
||||
case "get":
|
||||
return HttpMethod.GET;
|
||||
case "post":
|
||||
return HttpMethod.POST;
|
||||
case "head":
|
||||
return HttpMethod.HEAD;
|
||||
case "put":
|
||||
return HttpMethod.PUT;
|
||||
case "delete":
|
||||
return HttpMethod.DELETE;
|
||||
case "options":
|
||||
return HttpMethod.OPTIONS;
|
||||
case "trace":
|
||||
return HttpMethod.TRACE;
|
||||
case "connect":
|
||||
return HttpMethod.CONNECT;
|
||||
default:
|
||||
return HttpMethod.UNKNOWN;
|
||||
}
|
||||
}
|
||||
public uint MaximumHeaderLength { get; set; } = HttpPacketHelpers.DefaultMaximumHeaderLength;
|
||||
public uint MaximumContentLength { get; set; } = HttpPacketHelpers.DefaultMaximumContentLength;
|
||||
public int MaximumHeaderCount { get; set; } = HttpPacketHelpers.DefaultMaximumHeaderCount;
|
||||
public int MaximumFormFields { get; set; } = HttpPacketHelpers.DefaultMaximumFormFields;
|
||||
public int MaximumFormKeyLength { get; set; } = HttpPacketHelpers.DefaultMaximumFormKeyLength;
|
||||
public int MaximumFormValueLength { get; set; } = HttpPacketHelpers.DefaultMaximumFormValueLength;
|
||||
public int MaximumMultipartPartLength { get; set; } = HttpPacketHelpers.DefaultMaximumMultipartPartLength;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "HTTPRequestPacket"
|
||||
+ "\n\tVersion: " + Version
|
||||
+ "\n\tMethod: " + Method
|
||||
+ "\n\tURL: " + URL
|
||||
+ "\n\tMessage: " + (Message != null ? Message.Length.ToString() : "NULL");
|
||||
}
|
||||
=> $"HTTPRequestPacket\n\tVersion: {Version}\n\tMethod: {Method}\n\tURL: {URL}" +
|
||||
$"\n\tMessage: {(Message == null ? "NULL" : Message.Length.ToString())}";
|
||||
|
||||
public override long Parse(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
string[] sMethod = null;
|
||||
string[] sLines = null;
|
||||
ValidateBounds(data, offset, ends);
|
||||
var originalOffset = offset;
|
||||
|
||||
uint headerSize = 0;
|
||||
|
||||
for (uint i = offset; i < ends - 3; i++)
|
||||
{
|
||||
if (data[i] == '\r' && data[i + 1] == '\n'
|
||||
&& data[i + 2] == '\r' && data[i + 3] == '\n')
|
||||
{
|
||||
sLines = Encoding.ASCII.GetString(data, (int)offset, (int)(i - offset)).Split(new string[] { "\r\n" },
|
||||
StringSplitOptions.None);
|
||||
|
||||
headerSize = i + 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (headerSize == 0)
|
||||
if (!HttpPacketHelpers.TryFindHeaderEnd(
|
||||
data, offset, ends, MaximumHeaderLength, out var bodyOffset))
|
||||
return -1;
|
||||
|
||||
var lines = HttpPacketHelpers.ReadHeaderLines(
|
||||
data, offset, bodyOffset, MaximumHeaderCount);
|
||||
|
||||
if (lines.Length == 0)
|
||||
return 0;
|
||||
|
||||
Cookies = new StringKeyList();
|
||||
PostForms = new KeyList<string, object>();
|
||||
Query = new StringKeyList();
|
||||
Headers = new StringKeyList();
|
||||
Message = null;
|
||||
|
||||
sMethod = sLines[0].Split(' ');
|
||||
Method = GetMethod(sMethod[0].Trim());
|
||||
var requestLine = lines[0].Split(new[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (requestLine.Length != 3)
|
||||
return 0;
|
||||
|
||||
if (sMethod.Length == 3)
|
||||
RawMethod = requestLine[0];
|
||||
Method = GetMethod(RawMethod);
|
||||
Version = requestLine[2].Trim();
|
||||
|
||||
var target = requestLine[1].Trim();
|
||||
if (Uri.TryCreate(target, UriKind.Absolute, out var absoluteUri))
|
||||
target = absoluteUri.PathAndQuery;
|
||||
|
||||
var queryIndex = target.IndexOf('?');
|
||||
var rawFilename = queryIndex < 0 ? target : target.Substring(0, queryIndex);
|
||||
Filename = WebUtility.UrlDecode(rawFilename);
|
||||
URL = WebUtility.UrlDecode(target);
|
||||
|
||||
var hasContentLength = false;
|
||||
|
||||
for (var i = 1; i < lines.Length; i++)
|
||||
{
|
||||
sMethod[1] = WebUtility.UrlDecode(sMethod[1]);
|
||||
if (sMethod[1].Length >= 7)
|
||||
var separator = lines[i].IndexOf(':');
|
||||
if (separator <= 0)
|
||||
return 0;
|
||||
|
||||
var name = lines[i].Substring(0, separator).Trim();
|
||||
var value = lines[i].Substring(separator + 1).Trim();
|
||||
|
||||
if (string.Equals(name, "transfer-encoding", StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("HTTP Transfer-Encoding is not supported.");
|
||||
|
||||
if (string.Equals(name, "content-length", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (sMethod[1].StartsWith("http://"))
|
||||
{
|
||||
sMethod[1] = sMethod[1].Substring(sMethod[1].IndexOf("/", 7));
|
||||
}
|
||||
if (hasContentLength)
|
||||
throw new InvalidDataException("Duplicate HTTP Content-Length headers are not accepted.");
|
||||
|
||||
hasContentLength = true;
|
||||
}
|
||||
|
||||
URL = sMethod[1].Trim();
|
||||
Headers[name] = value;
|
||||
|
||||
if (URL.IndexOf("?", 0) != -1)
|
||||
if (string.Equals(name, "cookie", StringComparison.OrdinalIgnoreCase))
|
||||
ParseCookies(value);
|
||||
}
|
||||
|
||||
if (queryIndex >= 0 && queryIndex + 1 < target.Length)
|
||||
ParseQuery(target.Substring(queryIndex + 1));
|
||||
|
||||
var contentLength = 0u;
|
||||
if (hasContentLength && !uint.TryParse(
|
||||
Headers["content-length"],
|
||||
NumberStyles.None,
|
||||
CultureInfo.InvariantCulture,
|
||||
out contentLength))
|
||||
throw new InvalidDataException("HTTP Content-Length is invalid.");
|
||||
|
||||
if (MaximumContentLength > 0 && contentLength > MaximumContentLength)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP content length of {contentLength} bytes exceeds the {MaximumContentLength}-byte limit.");
|
||||
|
||||
var availableBody = ends - bodyOffset;
|
||||
if (availableBody < contentLength)
|
||||
return -(long)(contentLength - availableBody);
|
||||
|
||||
var contentType = Headers["content-type"];
|
||||
if (Method == HttpMethod.POST &&
|
||||
(string.IsNullOrEmpty(contentType) ||
|
||||
contentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
ParseUrlEncodedForm(Encoding.UTF8.GetString(data, (int)bodyOffset, (int)contentLength));
|
||||
}
|
||||
else if (Method == HttpMethod.POST &&
|
||||
contentType.StartsWith("multipart/form-data", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!TryParseMultipart(data, bodyOffset, contentLength, contentType))
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Message = data.Clip(bodyOffset, contentLength);
|
||||
}
|
||||
|
||||
return bodyOffset - originalOffset + contentLength;
|
||||
}
|
||||
|
||||
private static HttpMethod GetMethod(string method)
|
||||
{
|
||||
if (string.Equals(method, "GET", StringComparison.OrdinalIgnoreCase)) return HttpMethod.GET;
|
||||
if (string.Equals(method, "POST", StringComparison.OrdinalIgnoreCase)) return HttpMethod.POST;
|
||||
if (string.Equals(method, "HEAD", StringComparison.OrdinalIgnoreCase)) return HttpMethod.HEAD;
|
||||
if (string.Equals(method, "PUT", StringComparison.OrdinalIgnoreCase)) return HttpMethod.PUT;
|
||||
if (string.Equals(method, "DELETE", StringComparison.OrdinalIgnoreCase)) return HttpMethod.DELETE;
|
||||
if (string.Equals(method, "OPTIONS", StringComparison.OrdinalIgnoreCase)) return HttpMethod.OPTIONS;
|
||||
if (string.Equals(method, "TRACE", StringComparison.OrdinalIgnoreCase)) return HttpMethod.TRACE;
|
||||
if (string.Equals(method, "CONNECT", StringComparison.OrdinalIgnoreCase)) return HttpMethod.CONNECT;
|
||||
return HttpMethod.UNKNOWN;
|
||||
}
|
||||
|
||||
private void ParseCookies(string header)
|
||||
{
|
||||
foreach (var segment in header.Split(';'))
|
||||
{
|
||||
var cookie = segment.Trim();
|
||||
if (cookie.Length == 0)
|
||||
continue;
|
||||
|
||||
var separator = cookie.IndexOf('=');
|
||||
var name = separator < 0 ? cookie : cookie.Substring(0, separator).Trim();
|
||||
var value = separator < 0 ? string.Empty : cookie.Substring(separator + 1).Trim();
|
||||
if (!Cookies.ContainsKey(name))
|
||||
Cookies.Add(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseQuery(string query)
|
||||
{
|
||||
foreach (var segment in query.Split('&'))
|
||||
{
|
||||
var separator = segment.IndexOf('=');
|
||||
var name = WebUtility.UrlDecode(separator < 0 ? segment : segment.Substring(0, separator));
|
||||
var value = separator < 0 ? null : WebUtility.UrlDecode(segment.Substring(separator + 1));
|
||||
if (!Query.ContainsKey(name))
|
||||
Query.Add(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
private void ParseUrlEncodedForm(string form)
|
||||
{
|
||||
if (form.Length == 0)
|
||||
return;
|
||||
|
||||
var fieldCount = 0;
|
||||
var start = 0;
|
||||
StringBuilder unknown = null;
|
||||
var hasUnknownValue = false;
|
||||
|
||||
while (start <= form.Length)
|
||||
{
|
||||
fieldCount++;
|
||||
EnsureWithinLimit(fieldCount, MaximumFormFields, "form fields");
|
||||
|
||||
var end = form.IndexOf('&', start);
|
||||
if (end < 0)
|
||||
end = form.Length;
|
||||
|
||||
var separator = form.IndexOf('=', start, end - start);
|
||||
if (separator >= 0)
|
||||
{
|
||||
Filename = URL.Split(new char[] { '?' }, 2)[0];
|
||||
var key = DecodeFormComponent(form.Substring(start, separator - start));
|
||||
var value = DecodeFormComponent(form.Substring(separator + 1, end - separator - 1));
|
||||
EnsureStringWithinLimit(key, MaximumFormKeyLength, "form key");
|
||||
EnsureStringWithinLimit(value, MaximumFormValueLength, "form value");
|
||||
|
||||
if (string.Equals(key, "unknown", StringComparison.Ordinal))
|
||||
{
|
||||
if (unknown == null)
|
||||
unknown = new StringBuilder(value.Length);
|
||||
else
|
||||
unknown.Clear();
|
||||
unknown.Append(value);
|
||||
hasUnknownValue = true;
|
||||
}
|
||||
|
||||
PostForms[key] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Filename = URL;
|
||||
var value = DecodeFormComponent(form.Substring(start, end - start));
|
||||
EnsureStringWithinLimit(value, MaximumFormValueLength, "form value");
|
||||
|
||||
if (unknown == null)
|
||||
unknown = new StringBuilder(value.Length);
|
||||
|
||||
EnsureStringWithinLimit(
|
||||
unknown.Length + (hasUnknownValue ? 1 : 0) + value.Length,
|
||||
MaximumFormValueLength,
|
||||
"combined form value");
|
||||
|
||||
if (hasUnknownValue)
|
||||
unknown.Append('&');
|
||||
unknown.Append(value);
|
||||
hasUnknownValue = true;
|
||||
}
|
||||
|
||||
if (Filename.IndexOf("%", 0) != -1)
|
||||
{
|
||||
Filename = WebUtility.UrlDecode(Filename);
|
||||
}
|
||||
|
||||
Version = sMethod[2].Trim();
|
||||
if (end == form.Length)
|
||||
break;
|
||||
start = end + 1;
|
||||
}
|
||||
|
||||
// Read all headers
|
||||
if (unknown != null)
|
||||
PostForms["unknown"] = unknown.ToString();
|
||||
}
|
||||
|
||||
for (int i = 1; i < sLines.Length; i++)
|
||||
private bool TryParseMultipart(
|
||||
byte[] data,
|
||||
uint bodyOffset,
|
||||
uint contentLength,
|
||||
string contentType)
|
||||
{
|
||||
if (!TryGetMultipartBoundary(contentType, out var boundary))
|
||||
return false;
|
||||
|
||||
var delimiter = "--" + boundary;
|
||||
var body = Encoding.UTF8.GetString(data, (int)bodyOffset, (int)contentLength);
|
||||
var position = 0;
|
||||
var fieldCount = 0;
|
||||
|
||||
while (position < body.Length)
|
||||
{
|
||||
if (sLines[i] == string.Empty)
|
||||
{
|
||||
// Invalid header
|
||||
return 0;
|
||||
}
|
||||
var delimiterStart = body.IndexOf(delimiter, position, StringComparison.Ordinal);
|
||||
if (delimiterStart < 0)
|
||||
return false;
|
||||
|
||||
if (sLines[i].IndexOf(':') == -1)
|
||||
{
|
||||
// Invalid header
|
||||
return 0;
|
||||
}
|
||||
position = delimiterStart + delimiter.Length;
|
||||
if (position + 2 <= body.Length &&
|
||||
string.CompareOrdinal(body, position, "--", 0, 2) == 0)
|
||||
return true;
|
||||
|
||||
string[] header = sLines[i].Split(new char[] { ':' }, 2);
|
||||
if (position + 2 > body.Length ||
|
||||
string.CompareOrdinal(body, position, "\r\n", 0, 2) != 0)
|
||||
return false;
|
||||
position += 2;
|
||||
|
||||
header[0] = header[0].ToLower();
|
||||
Headers[header[0]] = header[1].Trim();
|
||||
var nextDelimiter = body.IndexOf("\r\n" + delimiter, position, StringComparison.Ordinal);
|
||||
if (nextDelimiter < 0)
|
||||
return false;
|
||||
|
||||
if (header[0] == "cookie")
|
||||
{
|
||||
string[] cookies = header[1].Split(';');
|
||||
var partLength = nextDelimiter - position;
|
||||
EnsureWithinLimit(partLength, MaximumMultipartPartLength, "multipart part length");
|
||||
|
||||
foreach (string cookie in cookies)
|
||||
{
|
||||
if (cookie.IndexOf('=') != -1)
|
||||
{
|
||||
string[] splitCookie = cookie.Split('=');
|
||||
splitCookie[0] = splitCookie[0].Trim();
|
||||
splitCookie[1] = splitCookie[1].Trim();
|
||||
if (!Cookies.ContainsKey(splitCookie[0].Trim()))
|
||||
Cookies.Add(splitCookie[0], splitCookie[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Cookies.ContainsKey(cookie.Trim()))
|
||||
{
|
||||
Cookies.Add(cookie.Trim(), string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var headerEnd = body.IndexOf("\r\n\r\n", position, partLength, StringComparison.Ordinal);
|
||||
if (headerEnd < 0)
|
||||
return false;
|
||||
|
||||
var nameStart = body.IndexOf("name=\"", position, headerEnd - position, StringComparison.OrdinalIgnoreCase);
|
||||
if (nameStart < 0)
|
||||
return false;
|
||||
nameStart += 6;
|
||||
|
||||
var nameEnd = body.IndexOf('"', nameStart, headerEnd - nameStart);
|
||||
if (nameEnd < 0 || nameEnd > headerEnd)
|
||||
return false;
|
||||
|
||||
fieldCount++;
|
||||
EnsureWithinLimit(fieldCount, MaximumFormFields, "form fields");
|
||||
|
||||
var name = body.Substring(nameStart, nameEnd - nameStart);
|
||||
EnsureStringWithinLimit(name, MaximumFormKeyLength, "form key");
|
||||
|
||||
var valueStart = headerEnd + 4;
|
||||
PostForms[name] = body.Substring(valueStart, nextDelimiter - valueStart);
|
||||
position = nextDelimiter + 2;
|
||||
}
|
||||
|
||||
// Query String
|
||||
if (URL.IndexOf("?", 0) != -1)
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string DecodeFormComponent(string value)
|
||||
=> WebUtility.HtmlDecode(WebUtility.UrlDecode(value));
|
||||
|
||||
private static bool TryGetMultipartBoundary(string contentType, out string boundary)
|
||||
{
|
||||
boundary = null;
|
||||
var parameterStart = contentType.IndexOf(';');
|
||||
while (parameterStart >= 0 && parameterStart + 1 < contentType.Length)
|
||||
{
|
||||
string[] SQ = URL.Split(new char[] { '?' }, 2)[1].Split('&');
|
||||
foreach (string S in SQ)
|
||||
{
|
||||
if (S.IndexOf("=", 0) != -1)
|
||||
{
|
||||
string[] qp = S.Split(new char[] { '=' }, 2);
|
||||
var parameterEnd = contentType.IndexOf(';', parameterStart + 1);
|
||||
if (parameterEnd < 0)
|
||||
parameterEnd = contentType.Length;
|
||||
|
||||
if (!Query.ContainsKey(WebUtility.UrlDecode(qp[0])))
|
||||
{
|
||||
Query.Add(WebUtility.UrlDecode(qp[0]), WebUtility.UrlDecode(qp[1]));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Query.ContainsKey(WebUtility.UrlDecode(S)))
|
||||
{
|
||||
Query.Add(WebUtility.UrlDecode(S), null);
|
||||
}
|
||||
}
|
||||
var parameter = contentType.Substring(
|
||||
parameterStart + 1,
|
||||
parameterEnd - parameterStart - 1).Trim();
|
||||
var separator = parameter.IndexOf('=');
|
||||
if (separator > 0 && string.Equals(
|
||||
parameter.Substring(0, separator).Trim(),
|
||||
"boundary",
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
boundary = parameter.Substring(separator + 1).Trim().Trim('"');
|
||||
return boundary.Length > 0 && boundary.IndexOfAny(new[] { '\r', '\n' }) < 0;
|
||||
}
|
||||
|
||||
parameterStart = parameterEnd < contentType.Length ? parameterEnd : -1;
|
||||
}
|
||||
|
||||
// Post Content-Length
|
||||
if (Method == HttpMethod.POST)
|
||||
{
|
||||
try
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint postSize = uint.Parse(Headers["content-length"]);
|
||||
private static void EnsureStringWithinLimit(string value, int limit, string kind)
|
||||
=> EnsureStringWithinLimit(value?.Length ?? 0, limit, kind);
|
||||
|
||||
// check limit
|
||||
if (postSize > data.Length - headerSize)
|
||||
return -(postSize - (data.Length - headerSize));
|
||||
private static void EnsureStringWithinLimit(int length, int limit, string kind)
|
||||
=> EnsureWithinLimit(length, limit, kind);
|
||||
|
||||
|
||||
if (
|
||||
Headers["content-type"] == null
|
||||
|| Headers["content-type"] == ""
|
||||
|| Headers["content-type"].StartsWith("application/x-www-form-urlencoded"))
|
||||
{
|
||||
string[] PostVars = null;
|
||||
PostVars = Encoding.UTF8.GetString(data, (int)headerSize, (int)postSize).Split('&');
|
||||
for (int J = 0; J < PostVars.Length; J++)
|
||||
{
|
||||
if (PostVars[J].IndexOf("=") != -1)
|
||||
{
|
||||
string key = WebUtility.HtmlDecode(
|
||||
WebUtility.UrlDecode(PostVars[J].Split(new char[] { '=' }, 2)[0]));
|
||||
if (PostForms.Contains(key))
|
||||
PostForms[key] = WebUtility.HtmlDecode(
|
||||
WebUtility.UrlDecode(PostVars[J].Split(new char[] { '=' }, 2)[1]));
|
||||
else
|
||||
PostForms.Add(key, WebUtility.HtmlDecode(
|
||||
WebUtility.UrlDecode(PostVars[J].Split(new char[] { '=' }, 2)[1])));
|
||||
}
|
||||
else
|
||||
if (PostForms.Contains("unknown"))
|
||||
PostForms["unknown"] = PostForms["unknown"]
|
||||
+ "&" + WebUtility.HtmlDecode(WebUtility.UrlDecode(PostVars[J]));
|
||||
else
|
||||
PostForms.Add("unknown", WebUtility.HtmlDecode(WebUtility.UrlDecode(PostVars[J])));
|
||||
}
|
||||
}
|
||||
else if (Headers["content-type"].StartsWith("multipart/form-data"))
|
||||
{
|
||||
int st = 1;
|
||||
int ed = 0;
|
||||
string strBoundry = "--" + Headers["content-type"].Substring(
|
||||
Headers["content-type"].IndexOf("boundary=", 0) + 9);
|
||||
|
||||
string[] sc = Encoding.UTF8.GetString(data, (int)headerSize, (int)postSize).Split(
|
||||
new string[] { strBoundry }, StringSplitOptions.None);
|
||||
|
||||
|
||||
for (int j = 1; j < sc.Length - 1; j++)
|
||||
{
|
||||
string[] ps = sc[j].Split(new string[] { "\r\n\r\n" }, 2, StringSplitOptions.None);
|
||||
ps[1] = ps[1].Substring(0, ps[1].Length - 2); // remove the empty line
|
||||
st = ps[0].IndexOf("name=", 0) + 6;
|
||||
ed = ps[0].IndexOf("\"", st);
|
||||
PostForms.Add(ps[0].Substring(st, ed - st), ps[1]);
|
||||
}
|
||||
}
|
||||
//else if (Headers["content-type"] == "application/json")
|
||||
//{
|
||||
// var json = DC.Clip(data, headerSize, postSize);
|
||||
//}
|
||||
else
|
||||
{
|
||||
//PostForms.Add(Headers["content-type"], Encoding.Default.GetString( ));
|
||||
Message = data.Clip(headerSize, postSize);
|
||||
}
|
||||
|
||||
return headerSize + postSize;
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return headerSize;
|
||||
private static void EnsureWithinLimit(int value, int limit, string kind)
|
||||
{
|
||||
if (limit > 0 && value > limit)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP {kind} of {value} exceeds the configured limit of {limit}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,217 +1,210 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2017 Ahmed Kh. Zamil
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
using Esiur.Data;
|
||||
using Esiur.Misc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Esiur.Misc;
|
||||
using Esiur.Data;
|
||||
|
||||
namespace Esiur.Net.Packets.Http;
|
||||
|
||||
public class HttpResponsePacket : Packet
|
||||
{
|
||||
|
||||
public StringKeyList Headers { get; } = new StringKeyList(true);
|
||||
public string Version { get; set; } = "HTTP/1.1";
|
||||
|
||||
public byte[] Message;
|
||||
public HttpResponseCode Number { get; set; } = HttpResponseCode.OK;
|
||||
public string Text;
|
||||
|
||||
public List<HttpCookie> Cookies { get; } = new List<HttpCookie>();
|
||||
public bool Handled;
|
||||
|
||||
public uint MaximumHeaderLength { get; set; } = HttpPacketHelpers.DefaultMaximumHeaderLength;
|
||||
public uint MaximumContentLength { get; set; } = HttpPacketHelpers.DefaultMaximumContentLength;
|
||||
public int MaximumHeaderCount { get; set; } = HttpPacketHelpers.DefaultMaximumHeaderCount;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum response body accepted by <see cref="Compose(HttpComposeOption)"/>.
|
||||
/// Zero preserves the legacy behavior of allowing any body that fits in a managed array.
|
||||
/// </summary>
|
||||
public uint MaximumComposedContentLength { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "HTTPResponsePacket"
|
||||
+ "\n\tVersion: " + Version
|
||||
//+ "\n\tMethod: " + Method
|
||||
//+ "\n\tURL: " + URL
|
||||
+ "\n\tMessage: " + (Message != null ? Message.Length.ToString() : "NULL");
|
||||
}
|
||||
=> $"HTTPResponsePacket\n\tVersion: {Version}" +
|
||||
$"\n\tMessage: {(Message == null ? "NULL" : Message.Length.ToString())}";
|
||||
|
||||
private string MakeHeader(HttpComposeOption options)
|
||||
private byte[] ComposeHeader(HttpComposeOption options)
|
||||
{
|
||||
string header = $"{Version} {(int)Number} {Text}\r\nServer: Esiur {Global.Version}\r\nDate: {DateTime.Now.ToUniversalTime().ToString("r")}\r\n";
|
||||
|
||||
if (options == HttpComposeOption.AllCalculateLength)
|
||||
Headers["Content-Length"] = Message?.Length.ToString() ?? "0";
|
||||
Headers["Content-Length"] = Message?.Length.ToString(CultureInfo.InvariantCulture) ?? "0";
|
||||
|
||||
foreach (var kv in Headers)
|
||||
header += kv.Key + ": " + kv.Value + "\r\n";
|
||||
var header = new StringBuilder(256);
|
||||
header.Append(Version)
|
||||
.Append(' ')
|
||||
.Append((int)Number)
|
||||
.Append(' ')
|
||||
.Append(Text ?? string.Empty)
|
||||
.Append("\r\nServer: Esiur ")
|
||||
.Append(Global.Version)
|
||||
.Append("\r\nDate: ")
|
||||
.Append(DateTime.UtcNow.ToString("r", CultureInfo.InvariantCulture))
|
||||
.Append("\r\n");
|
||||
|
||||
foreach (var entry in Headers)
|
||||
header.Append(entry.Key).Append(": ").Append(entry.Value).Append("\r\n");
|
||||
foreach (var cookie in Cookies)
|
||||
header.Append("Set-Cookie: ").Append(cookie).Append("\r\n");
|
||||
|
||||
// Set-Cookie: ckGeneric=CookieBody; expires=Sun, 30-Dec-2007 21:00:00 GMT; path=/
|
||||
// Set-Cookie: ASPSESSIONIDQABBDSQA=IPDPMMMALDGFLMICEJIOCIPM; path=/
|
||||
|
||||
foreach (var Cookie in Cookies)
|
||||
header += "Set-Cookie: " + Cookie.ToString() + "\r\n";
|
||||
|
||||
|
||||
header += "\r\n";
|
||||
|
||||
return header;
|
||||
header.Append("\r\n");
|
||||
return Encoding.ASCII.GetBytes(header.ToString());
|
||||
}
|
||||
|
||||
|
||||
public bool Compose(HttpComposeOption options)
|
||||
{
|
||||
List<byte> msg = new List<byte>();
|
||||
var header = options == HttpComposeOption.DataOnly
|
||||
? Array.Empty<byte>()
|
||||
: ComposeHeader(options);
|
||||
var body = options == HttpComposeOption.SpecifiedHeadersOnly || Message == null
|
||||
? Array.Empty<byte>()
|
||||
: Message;
|
||||
|
||||
if (options != HttpComposeOption.DataOnly)
|
||||
{
|
||||
msg.AddRange(Encoding.UTF8.GetBytes(MakeHeader(options)));
|
||||
}
|
||||
if (MaximumComposedContentLength > 0 && body.LongLength > MaximumComposedContentLength)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP content length of {body.LongLength} bytes exceeds the {MaximumComposedContentLength}-byte limit.");
|
||||
|
||||
if (options != HttpComposeOption.SpecifiedHeadersOnly)
|
||||
{
|
||||
if (Message != null)
|
||||
msg.AddRange(Message);
|
||||
}
|
||||
|
||||
Data = msg.ToArray();
|
||||
Data = new byte[checked(header.Length + body.Length)];
|
||||
if (header.Length > 0)
|
||||
Buffer.BlockCopy(header, 0, Data, 0, header.Length);
|
||||
if (body.Length > 0)
|
||||
Buffer.BlockCopy(body, 0, Data, header.Length, body.Length);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool Compose()
|
||||
{
|
||||
return Compose(HttpComposeOption.AllDontCalculateLength);
|
||||
}
|
||||
public override bool Compose() => Compose(HttpComposeOption.AllDontCalculateLength);
|
||||
|
||||
public override long Parse(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
string[] sMethod = null;
|
||||
string[] sLines = null;
|
||||
ValidateBounds(data, offset, ends);
|
||||
var originalOffset = offset;
|
||||
|
||||
uint headerSize = 0;
|
||||
|
||||
for (uint i = offset; i < ends - 3; i++)
|
||||
{
|
||||
if (data[i] == '\r' && data[i + 1] == '\n'
|
||||
&& data[i + 2] == '\r' && data[i + 3] == '\n')
|
||||
{
|
||||
sLines = Encoding.ASCII.GetString(data, (int)offset, (int)(i - offset)).Split(new string[] { "\r\n" },
|
||||
StringSplitOptions.None);
|
||||
|
||||
headerSize = i + 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (headerSize == 0)
|
||||
if (!HttpPacketHelpers.TryFindHeaderEnd(
|
||||
data, offset, ends, MaximumHeaderLength, out var bodyOffset))
|
||||
return -1;
|
||||
|
||||
var lines = HttpPacketHelpers.ReadHeaderLines(
|
||||
data, offset, bodyOffset, MaximumHeaderCount);
|
||||
|
||||
sMethod = sLines[0].Split(' ');
|
||||
|
||||
if (sMethod.Length == 3)
|
||||
{
|
||||
Version = sMethod[0].Trim();
|
||||
Number = (HttpResponseCode)Convert.ToInt32(sMethod[1].Trim());
|
||||
Text = sMethod[2];
|
||||
}
|
||||
|
||||
// Read all headers
|
||||
|
||||
for (int i = 1; i < sLines.Length; i++)
|
||||
{
|
||||
if (sLines[i] == string.Empty)
|
||||
{
|
||||
// Invalid header
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sLines[i].IndexOf(':') == -1)
|
||||
{
|
||||
// Invalid header
|
||||
return 0;
|
||||
}
|
||||
|
||||
string[] header = sLines[i].Split(new char[] { ':' }, 2);
|
||||
|
||||
header[0] = header[0].ToLower();
|
||||
Headers[header[0]] = header[1].Trim();
|
||||
|
||||
//Set-Cookie: NAME=VALUE; expires=DATE;
|
||||
|
||||
if (header[0] == "set-cookie")
|
||||
{
|
||||
string[] cookie = header[1].Split(';');
|
||||
|
||||
if (cookie.Length >= 1)
|
||||
{
|
||||
string[] splitCookie = cookie[0].Split('=');
|
||||
HttpCookie c = new HttpCookie(splitCookie[0], splitCookie[1]);
|
||||
|
||||
for (int j = 1; j < cookie.Length; j++)
|
||||
{
|
||||
splitCookie = cookie[j].Split('=');
|
||||
switch (splitCookie[0].ToLower())
|
||||
{
|
||||
case "domain":
|
||||
c.Domain = splitCookie[1];
|
||||
break;
|
||||
case "path":
|
||||
c.Path = splitCookie[1];
|
||||
break;
|
||||
case "httponly":
|
||||
c.HttpOnly = true;
|
||||
break;
|
||||
case "expires":
|
||||
// Wed, 13-Jan-2021 22:23:01 GMT
|
||||
c.Expires = DateTime.Parse(splitCookie[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Content-Length
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
uint contentLength = uint.Parse(Headers["content-length"]);
|
||||
|
||||
// check limit
|
||||
if (contentLength > data.Length - headerSize)
|
||||
{
|
||||
return contentLength - (data.Length - headerSize);
|
||||
}
|
||||
|
||||
Message = data.Clip(offset, contentLength);
|
||||
|
||||
return headerSize + contentLength;
|
||||
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (lines.Length == 0)
|
||||
return 0;
|
||||
|
||||
var statusLine = lines[0].Split(new[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (statusLine.Length < 2 ||
|
||||
!int.TryParse(statusLine[1], NumberStyles.None, CultureInfo.InvariantCulture, out var statusCode))
|
||||
return 0;
|
||||
|
||||
Version = statusLine[0];
|
||||
Number = (HttpResponseCode)statusCode;
|
||||
Text = statusLine.Length == 3 ? statusLine[2] : string.Empty;
|
||||
Headers.Clear();
|
||||
Cookies.Clear();
|
||||
Message = null;
|
||||
|
||||
var hasContentLength = false;
|
||||
|
||||
for (var i = 1; i < lines.Length; i++)
|
||||
{
|
||||
var separator = lines[i].IndexOf(':');
|
||||
if (separator <= 0)
|
||||
return 0;
|
||||
|
||||
var name = lines[i].Substring(0, separator).Trim();
|
||||
var value = lines[i].Substring(separator + 1).Trim();
|
||||
|
||||
if (string.Equals(name, "transfer-encoding", StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException("HTTP Transfer-Encoding is not supported.");
|
||||
|
||||
if (string.Equals(name, "content-length", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (hasContentLength)
|
||||
throw new InvalidDataException("Duplicate HTTP Content-Length headers are not accepted.");
|
||||
|
||||
hasContentLength = true;
|
||||
}
|
||||
|
||||
Headers.Add(name, value);
|
||||
|
||||
if (string.Equals(name, "set-cookie", StringComparison.OrdinalIgnoreCase) &&
|
||||
TryParseCookie(value, out var cookie))
|
||||
Cookies.Add(cookie);
|
||||
}
|
||||
|
||||
var contentLengthHeader = Headers["content-length"];
|
||||
if (contentLengthHeader == null)
|
||||
{
|
||||
Message = Array.Empty<byte>();
|
||||
return bodyOffset - originalOffset;
|
||||
}
|
||||
|
||||
if (!uint.TryParse(
|
||||
contentLengthHeader,
|
||||
NumberStyles.None,
|
||||
CultureInfo.InvariantCulture,
|
||||
out var contentLength))
|
||||
return 0;
|
||||
|
||||
if (MaximumContentLength > 0 && contentLength > MaximumContentLength)
|
||||
throw new ParserLimitException(
|
||||
$"HTTP content length of {contentLength} bytes exceeds the {MaximumContentLength}-byte limit.");
|
||||
|
||||
var availableBody = ends - bodyOffset;
|
||||
if (availableBody < contentLength)
|
||||
return -(long)(contentLength - availableBody);
|
||||
|
||||
Message = data.Clip(bodyOffset, contentLength);
|
||||
return bodyOffset - originalOffset + contentLength;
|
||||
}
|
||||
|
||||
private static bool TryParseCookie(string header, out HttpCookie cookie)
|
||||
{
|
||||
cookie = default;
|
||||
var segments = header.Split(';');
|
||||
if (segments.Length == 0)
|
||||
return false;
|
||||
|
||||
var nameValueSeparator = segments[0].IndexOf('=');
|
||||
if (nameValueSeparator <= 0)
|
||||
return false;
|
||||
|
||||
cookie = new HttpCookie(
|
||||
segments[0].Substring(0, nameValueSeparator).Trim(),
|
||||
segments[0].Substring(nameValueSeparator + 1).Trim());
|
||||
|
||||
for (var i = 1; i < segments.Length; i++)
|
||||
{
|
||||
var segment = segments[i].Trim();
|
||||
var separator = segment.IndexOf('=');
|
||||
var name = separator < 0 ? segment : segment.Substring(0, separator).Trim();
|
||||
var value = separator < 0 ? string.Empty : segment.Substring(separator + 1).Trim();
|
||||
|
||||
if (string.Equals(name, "domain", StringComparison.OrdinalIgnoreCase))
|
||||
cookie.Domain = value;
|
||||
else if (string.Equals(name, "path", StringComparison.OrdinalIgnoreCase))
|
||||
cookie.Path = value;
|
||||
else if (string.Equals(name, "httponly", StringComparison.OrdinalIgnoreCase))
|
||||
cookie.HttpOnly = true;
|
||||
else if (string.Equals(name, "secure", StringComparison.OrdinalIgnoreCase))
|
||||
cookie.Secure = true;
|
||||
else if (string.Equals(name, "samesite", StringComparison.OrdinalIgnoreCase) &&
|
||||
Enum.TryParse(value, true, out HttpCookieSameSite sameSite))
|
||||
cookie.SameSite = sameSite;
|
||||
else if (string.Equals(name, "expires", StringComparison.OrdinalIgnoreCase) &&
|
||||
DateTime.TryParse(
|
||||
value,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out var expires))
|
||||
cookie.Expires = expires;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,367 +1,42 @@
|
||||
|
||||
/********************************************************************************\
|
||||
* Uruky Project *
|
||||
* *
|
||||
* Copyright (C) 2006 Ahmed Zamil - ahmed@dijlh.com *
|
||||
* http://www.dijlh.com *
|
||||
* *
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy *
|
||||
* of this software and associated documentation files (the "Software"), to deal *
|
||||
* in the Software without restriction, including without limitation the rights *
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell *
|
||||
* copies of the Software, and to permit persons to whom the Software is *
|
||||
* furnished to do so, subject to the following conditions: *
|
||||
* *
|
||||
* The above copyright notice and this permission notice shall be included in all *
|
||||
* copies or substantial portions of the Software. *
|
||||
* *
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR *
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE *
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER *
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, *
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE *
|
||||
* SOFTWARE. *
|
||||
* *
|
||||
* File: Packet.cs *
|
||||
* Description: Ethernet/ARP/IPv4/TCP/UDP Packet Decoding & Encoding Class *
|
||||
* Compatibility: .Net Framework 2.0 / Mono 1.1.8 *
|
||||
* *
|
||||
\********************************************************************************/
|
||||
|
||||
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using Esiur.Misc;
|
||||
using Esiur.Net.DataLink;
|
||||
using System.Net.NetworkInformation;
|
||||
using Esiur.Data;
|
||||
|
||||
namespace Esiur.Net.Packets;
|
||||
internal static class Functions
|
||||
{
|
||||
public static void AddData(ref byte[] dest, byte[] src)
|
||||
{
|
||||
int I = 0;
|
||||
if (src == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (dest != null)
|
||||
{
|
||||
I = dest.Length;
|
||||
Array.Resize(ref dest, dest.Length + src.Length);
|
||||
//dest = (byte[])Resize(dest, dest.Length + src.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
dest = new byte[src.Length];
|
||||
}
|
||||
Array.Copy(src, 0, dest, I, src.Length);
|
||||
}
|
||||
|
||||
/*
|
||||
public static Array Resize(Array array, int newSize)
|
||||
{
|
||||
Type myType = Type.GetType(array.GetType().FullName.TrimEnd('[', ']'));
|
||||
Array nA = Array.CreateInstance(myType, newSize);
|
||||
Array.Copy(array, nA, (newSize > array.Length ? array.Length : newSize));
|
||||
return nA;
|
||||
} */
|
||||
|
||||
//Computes the checksum used in IP, ARP..., ie the
|
||||
// "The 16 bit one's complement of the one 's complement sum
|
||||
//of all 16 bit words" as seen in RFCs
|
||||
// Returns a 4 characters hex string
|
||||
// data's lenght must be multiple of 4, else zero padding
|
||||
public static ushort IP_CRC16(byte[] data)
|
||||
{
|
||||
ulong Sum = 0;
|
||||
bool Padding = false;
|
||||
/// * Padding if needed
|
||||
if (data.Length % 2 != 0)
|
||||
{
|
||||
Array.Resize(ref data, data.Length + 1);
|
||||
//data = (byte[])Resize(data, data.Length + 1);
|
||||
Padding = true;
|
||||
}
|
||||
int count = data.Length;
|
||||
///* add 16-bit words */
|
||||
while (count > 0) //1)
|
||||
{
|
||||
///* this is the inner loop */
|
||||
Sum += GetInteger(data[count - 2], data[count - 1]);
|
||||
///* Fold 32-bit sum to 16-bit */
|
||||
while (Sum >> 16 != 0)
|
||||
{
|
||||
Sum = (Sum & 0XFFFF) + (Sum >> 16);
|
||||
}
|
||||
count -= 2;
|
||||
}
|
||||
/// * reverse padding
|
||||
if (Padding)
|
||||
{
|
||||
Array.Resize(ref data, data.Length - 1);
|
||||
//data = (byte[])Resize(data, data.Length - 1);
|
||||
}
|
||||
///* Return one's compliment of final sum.
|
||||
//return (ushort)(ushort.MaxValue - (ushort)Sum);
|
||||
return (ushort)(~Sum);
|
||||
}
|
||||
|
||||
public static ushort GetInteger(byte B1, byte B2)
|
||||
{
|
||||
return BitConverter.ToUInt16(new byte[] { B2, B1 }, 0);
|
||||
//return System.Convert.ToUInt16("&h" + GetHex(B1) + GetHex(B2));
|
||||
}
|
||||
|
||||
public static uint GetLong(byte B1, byte B2, byte B3, byte B4)
|
||||
{
|
||||
return BitConverter.ToUInt32(new byte[] { B4, B3, B2, B1 }, 0);
|
||||
//return System.Convert.ToUInt32("&h" + GetHex(B1) + GetHex(B2) + GetHex(B3) + GetHex(B4));
|
||||
}
|
||||
|
||||
public static string GetHex(byte B)
|
||||
{
|
||||
return (((B < 15) ? 0 + System.Convert.ToString(B, 16).ToUpper() : System.Convert.ToString(B, 16).ToUpper()));
|
||||
}
|
||||
|
||||
public static bool GetBit(uint B, byte Pos)
|
||||
{
|
||||
//return BitConverter.ToBoolean(BitConverter.GetBytes(B), Pos + 1);
|
||||
return (B & (uint)(Math.Pow(2, (Pos - 1)))) == (Math.Pow(2, (Pos - 1)));
|
||||
}
|
||||
|
||||
public static ushort RemoveBit(ushort I, byte Pos)
|
||||
{
|
||||
return (ushort)RemoveBit((uint)I, Pos);
|
||||
}
|
||||
|
||||
public static uint RemoveBit(uint I, byte Pos)
|
||||
{
|
||||
if (GetBit(I, Pos))
|
||||
{
|
||||
return I - (uint)(Math.Pow(2, (Pos - 1)));
|
||||
}
|
||||
else
|
||||
{
|
||||
return I;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SplitInteger(ushort I, ref byte BLeft, ref byte BRight)
|
||||
{
|
||||
byte[] b = BitConverter.GetBytes(I);
|
||||
BLeft = b[1];
|
||||
BRight = b[0];
|
||||
//BLeft = I >> 8;
|
||||
//BRight = (I << 8) >> 8;
|
||||
}
|
||||
|
||||
public static void SplitLong(uint I, ref byte BLeft, ref byte BLeftMiddle, ref byte BRightMiddle, ref byte BRight)
|
||||
{
|
||||
byte[] b = BitConverter.GetBytes(I);
|
||||
BLeft = b[3];
|
||||
BLeftMiddle = b[2];
|
||||
BRightMiddle = b[1];
|
||||
BRight = b[0];
|
||||
//BLeft = I >> 24;
|
||||
//BLeftMiddle = (I << 8) >> 24;
|
||||
//BRightMiddle = (I << 16) >> 24;
|
||||
//BRight = (I << 24) >> 24;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class PosixTime
|
||||
{
|
||||
ulong seconds;
|
||||
ulong microseconds;
|
||||
|
||||
PosixTime(ulong Seconds, ulong Microseconds)
|
||||
{
|
||||
seconds = Seconds;
|
||||
microseconds = Microseconds;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return seconds + "." + microseconds;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compatibility base for packet parsers and composers.
|
||||
/// </summary>
|
||||
public class Packet
|
||||
{
|
||||
//public EtherServer2.EthernetSource Source;
|
||||
|
||||
public PacketSource Source;
|
||||
|
||||
public DateTime Timestamp;
|
||||
|
||||
public enum PPPType : ushort
|
||||
{
|
||||
IP = 0x0021, // Internet Protocol version 4 [RFC1332]
|
||||
SDTP = 0x0049, // Serial Data Transport Protocol (PPP-SDTP) [RFC1963]
|
||||
IPv6HeaderCompression = 0x004f, // IPv6 Header Compression
|
||||
IPv6 = 0x0057, // Internet Protocol version 6 [RFC5072]
|
||||
W8021dHelloPacket = 0x0201, // 802.1d Hello Packets [RFC3518]
|
||||
IPv6ControlProtocol = 0x8057, // IPv6 Control Protocol [RFC5072]
|
||||
}
|
||||
|
||||
public enum ProtocolType : ushort
|
||||
{
|
||||
IP = 0x800, // IPv4
|
||||
ARP = 0x806, // Address Resolution Protocol
|
||||
IPv6 = 0x86DD, // IPv6
|
||||
FrameRelayARP = 0x0808, // Frame Relay ARP [RFC1701]
|
||||
VINESLoopback = 0x0BAE, // VINES Loopback [RFC1701]
|
||||
VINESEcho = 0x0BAF, // VINES ECHO [RFC1701]
|
||||
TransEtherBridging = 0x6558, // TransEther Bridging [RFC1701]
|
||||
RawFrameRelay = 0x6559, // Raw Frame Relay [RFC1701]
|
||||
IEE8021QVLAN = 0x8100, // IEEE 802.1Q VLAN-tagged frames (initially Wellfleet)
|
||||
SNMP = 0x814C, // SNMP [JKR1]
|
||||
TCPIP_Compression = 0x876B, // TCP/IP Compression [RFC1144]
|
||||
IPAutonomousSystems = 0x876C, // IP Autonomous Systems [RFC1701]
|
||||
SecureData = 0x876D, // Secure Data [RFC1701]
|
||||
PPP = 0x880B, // PPP [IANA]
|
||||
MPLS = 0x8847, // MPLS [RFC5332]
|
||||
MPLS_UpstreamAssignedLabel = 0x8848, // MPLS with upstream-assigned label [RFC5332]
|
||||
PPPoEDiscoveryStage = 0x8863, // PPPoE Discovery Stage [RFC2516]
|
||||
PPPoESessionStage = 0x8864, // PPPoE Session Stage [RFC2516]
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
public static void GetPacketMACAddresses(Packet packet, out byte[] srcMAC, out byte[] dstMAC)
|
||||
{
|
||||
|
||||
// get the node address
|
||||
Packet root = packet.RootPacket;
|
||||
if (root is TZSPPacket)
|
||||
{
|
||||
|
||||
TZSPPacket tp = (TZSPPacket)root;
|
||||
if (tp.Protocol == TZSPPacket.TZSPEncapsulatedProtocol.Ethernet)
|
||||
{
|
||||
EthernetPacket ep = (EthernetPacket)tp.SubPacket;
|
||||
srcMAC = ep.SourceMAC;
|
||||
dstMAC = ep.DestinationMAC;
|
||||
}
|
||||
else if (tp.Protocol == TZSPPacket.TZSPEncapsulatedProtocol.IEEE802_11)
|
||||
{
|
||||
W802_11Packet wp = (W802_11Packet)tp.SubPacket;
|
||||
srcMAC = wp.SA;
|
||||
dstMAC = wp.DA;
|
||||
}
|
||||
else
|
||||
{
|
||||
srcMAC = null;
|
||||
dstMAC = null;
|
||||
}
|
||||
}
|
||||
else if (root is EthernetPacket)
|
||||
{
|
||||
EthernetPacket ep = (EthernetPacket)root;
|
||||
srcMAC = ep.SourceMAC;
|
||||
dstMAC = ep.DestinationMAC;
|
||||
}
|
||||
else if (root is W802_11Packet)
|
||||
{
|
||||
W802_11Packet wp = (W802_11Packet)root;
|
||||
srcMAC = wp.SA;
|
||||
dstMAC = wp.DA;
|
||||
}
|
||||
else
|
||||
{
|
||||
srcMAC = null;
|
||||
dstMAC = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static void GetPacketAddresses(Packet packet, ref string srcMAC, ref string dstMAC, ref string srcIP, ref string dstIP)
|
||||
{
|
||||
|
||||
if (packet is TCPv4Packet)
|
||||
{
|
||||
if (packet.ParentPacket is IPv4Packet)
|
||||
{
|
||||
IPv4Packet ip = (IPv4Packet)packet.ParentPacket;
|
||||
srcIP = ip.SourceIP.ToString();
|
||||
dstIP = ip.DestinationIP.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// get the node address
|
||||
Packet root = packet.RootPacket;
|
||||
if (root is TZSPPacket)
|
||||
{
|
||||
|
||||
TZSPPacket tp = (TZSPPacket)root;
|
||||
if (tp.Protocol == TZSPPacket.TZSPEncapsulatedProtocol.Ethernet)
|
||||
{
|
||||
EthernetPacket ep = (EthernetPacket)tp.SubPacket;
|
||||
srcMAC = DC.GetPhysicalAddress(ep.SourceMAC, 0).ToString();
|
||||
dstMAC = DC.GetPhysicalAddress(ep.DestinationMAC, 0).ToString();
|
||||
}
|
||||
else if (tp.Protocol == TZSPPacket.TZSPEncapsulatedProtocol.IEEE802_11)
|
||||
{
|
||||
W802_11Packet wp = (W802_11Packet)tp.SubPacket;
|
||||
srcMAC = DC.GetPhysicalAddress(wp.SA, 0).ToString();
|
||||
dstMAC = DC.GetPhysicalAddress(wp.DA, 0).ToString();
|
||||
}
|
||||
}
|
||||
else if (root is EthernetPacket)
|
||||
{
|
||||
EthernetPacket ep = (EthernetPacket)root;
|
||||
srcMAC = DC.GetPhysicalAddress(ep.SourceMAC, 0).ToString();
|
||||
dstMAC = DC.GetPhysicalAddress(ep.DestinationMAC, 0).ToString();
|
||||
}
|
||||
else if (root is W802_11Packet)
|
||||
{
|
||||
W802_11Packet wp = (W802_11Packet)root;
|
||||
srcMAC = DC.GetPhysicalAddress(wp.SA, 0).ToString();
|
||||
dstMAC = DC.GetPhysicalAddress(wp.DA, 0).ToString();
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
//PosixTime Timeval;
|
||||
public byte[] Header;
|
||||
public byte[] Preamble;
|
||||
//public byte[] Payload;
|
||||
public byte[] Data;
|
||||
|
||||
public Packet SubPacket;
|
||||
public Packet ParentPacket;
|
||||
public virtual long Parse(byte[] data, uint offset, uint ends) { return 0; }
|
||||
public virtual bool Compose() { return false; }
|
||||
public virtual long Parse(byte[] data, uint offset, uint ends) => 0;
|
||||
|
||||
public Packet RootPacket
|
||||
public virtual bool Compose() => false;
|
||||
|
||||
protected static void ValidateBounds(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
get
|
||||
{
|
||||
Packet root = this;
|
||||
while (root.ParentPacket != null)
|
||||
root = root.ParentPacket;
|
||||
return root;
|
||||
}
|
||||
if (data == null)
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
if (ends > data.Length)
|
||||
throw new ArgumentOutOfRangeException(nameof(ends));
|
||||
if (offset > ends)
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
}
|
||||
|
||||
public Packet LeafPacket
|
||||
protected static bool TryGetMissingBytes(
|
||||
uint offset,
|
||||
uint ends,
|
||||
uint needed,
|
||||
out long parseResult)
|
||||
{
|
||||
get
|
||||
var available = ends - offset;
|
||||
if (available < needed)
|
||||
{
|
||||
Packet leaf = this;
|
||||
while (leaf.SubPacket != null)
|
||||
leaf = leaf.SubPacket;
|
||||
return leaf;
|
||||
parseResult = -(long)(needed - available);
|
||||
return true;
|
||||
}
|
||||
|
||||
parseResult = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/************************************ EOF *************************************/
|
||||
|
||||
|
||||
@@ -1,56 +1,26 @@
|
||||
/*
|
||||
|
||||
Copyright (c) 2017 Ahmed Kh. Zamil
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Esiur.Misc;
|
||||
using Esiur.Data;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Esiur.Net.Packets.WebSocket;
|
||||
|
||||
public class WebsocketPacket : Packet
|
||||
{
|
||||
private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(false, true);
|
||||
|
||||
public enum WSOpcode : byte
|
||||
{
|
||||
ContinuationFrame = 0x0, // %x0 denotes a continuation frame
|
||||
|
||||
TextFrame = 0x1, // %x1 denotes a text frame
|
||||
|
||||
BinaryFrame = 0x2, // %x2 denotes a binary frame
|
||||
|
||||
// %x3-7 are reserved for further non-control frames
|
||||
|
||||
ConnectionClose = 0x8, // %x8 denotes a connection close
|
||||
|
||||
Ping = 0x9, // %x9 denotes a ping
|
||||
|
||||
Pong = 0xA, // %xA denotes a pong
|
||||
|
||||
//* %xB-F are reserved for further control frames
|
||||
ContinuationFrame = 0x0,
|
||||
TextFrame = 0x1,
|
||||
BinaryFrame = 0x2,
|
||||
ConnectionClose = 0x8,
|
||||
Ping = 0x9,
|
||||
Pong = 0xA,
|
||||
}
|
||||
|
||||
public const ulong DefaultMaximumPayloadLength = 8 * 1024 * 1024;
|
||||
|
||||
public bool FIN;
|
||||
public bool RSV1;
|
||||
@@ -59,157 +29,243 @@ public class WebsocketPacket : Packet
|
||||
public WSOpcode Opcode;
|
||||
public bool Mask;
|
||||
public long PayloadLength;
|
||||
// public UInt32 MaskKey;
|
||||
public byte[] MaskKey;
|
||||
|
||||
public byte[] Message;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum accepted or composed payload. Set to zero to disable the limit.
|
||||
/// </summary>
|
||||
public ulong MaximumPayloadLength { get; set; } = DefaultMaximumPayloadLength;
|
||||
|
||||
/// <summary>
|
||||
/// Expected mask bit for an incoming frame. Servers set this to <c>true</c>,
|
||||
/// clients set it to <c>false</c>, and standalone packet parsing can leave it
|
||||
/// <c>null</c> to accept either direction.
|
||||
/// </summary>
|
||||
public bool? ExpectedMask { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "WebsocketPacket"
|
||||
+ "\n\tFIN: " + FIN
|
||||
+ "\n\tOpcode: " + Opcode
|
||||
+ "\n\tPayload: " + PayloadLength
|
||||
+ "\n\tMaskKey: " + MaskKey
|
||||
+ "\n\tMessage: " + (Message != null ? Message.Length.ToString() : "NULL");
|
||||
}
|
||||
=> $"WebsocketPacket\n\tFIN: {FIN}\n\tOpcode: {Opcode}\n\tPayload: {PayloadLength}" +
|
||||
$"\n\tMaskKey: {MaskKey}\n\tMessage: {(Message == null ? "NULL" : Message.Length.ToString())}";
|
||||
|
||||
public override bool Compose()
|
||||
{
|
||||
var pkt = new List<byte>();
|
||||
pkt.Add((byte)((FIN ? 0x80 : 0x0) |
|
||||
(RSV1 ? 0x40 : 0x0) |
|
||||
(RSV2 ? 0x20 : 0x0) |
|
||||
(RSV3 ? 0x10 : 0x0) |
|
||||
(byte)Opcode));
|
||||
var message = Message ?? Array.Empty<byte>();
|
||||
ValidateFrame(Opcode, FIN, (ulong)message.LongLength);
|
||||
ValidateApplicationPayload(Opcode, FIN, message);
|
||||
EnsureWithinLimit((ulong)message.LongLength);
|
||||
|
||||
// calculate length
|
||||
if (Message.Length > ushort.MaxValue)
|
||||
// 4 bytes
|
||||
var extendedLengthSize = message.Length <= 125
|
||||
? 0
|
||||
: message.Length <= ushort.MaxValue ? 2 : 8;
|
||||
var headerLength = 2 + extendedLengthSize + (Mask ? 4 : 0);
|
||||
Data = new byte[checked(headerLength + message.Length)];
|
||||
|
||||
var offset = 0;
|
||||
Data[offset++] = (byte)((FIN ? 0x80 : 0) |
|
||||
(RSV1 ? 0x40 : 0) |
|
||||
(RSV2 ? 0x20 : 0) |
|
||||
(RSV3 ? 0x10 : 0) |
|
||||
(byte)Opcode);
|
||||
|
||||
if (extendedLengthSize == 0)
|
||||
{
|
||||
pkt.Add((byte)((Mask ? 0x80 : 0x0) | 127));
|
||||
pkt.AddRange(((ulong)Message.LongCount()).ToBytes(Endian.Big));
|
||||
Data[offset++] = (byte)((Mask ? 0x80 : 0) | message.Length);
|
||||
}
|
||||
else if (Message.Length > 125)
|
||||
// 2 bytes
|
||||
else if (extendedLengthSize == 2)
|
||||
{
|
||||
pkt.Add((byte)((Mask ? 0x80 : 0x0) | 126));
|
||||
pkt.AddRange(((ushort)Message.Length).ToBytes(Endian.Big));
|
||||
Data[offset++] = (byte)((Mask ? 0x80 : 0) | 126);
|
||||
Data[offset++] = (byte)(message.Length >> 8);
|
||||
Data[offset++] = (byte)message.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
pkt.Add((byte)((Mask ? 0x80 : 0x0) | Message.Length));
|
||||
Data[offset++] = (byte)((Mask ? 0x80 : 0) | 127);
|
||||
var length = (ulong)message.LongLength;
|
||||
for (var shift = 56; shift >= 0; shift -= 8)
|
||||
Data[offset++] = (byte)(length >> shift);
|
||||
}
|
||||
|
||||
if (Mask)
|
||||
{
|
||||
pkt.AddRange(MaskKey);
|
||||
if (MaskKey == null || MaskKey.Length != 4)
|
||||
{
|
||||
MaskKey = new byte[4];
|
||||
using (var random = RandomNumberGenerator.Create())
|
||||
random.GetBytes(MaskKey);
|
||||
}
|
||||
|
||||
Buffer.BlockCopy(MaskKey, 0, Data, offset, MaskKey.Length);
|
||||
offset += MaskKey.Length;
|
||||
|
||||
for (var i = 0; i < message.Length; i++)
|
||||
Data[offset + i] = (byte)(message[i] ^ MaskKey[i & 3]);
|
||||
}
|
||||
else if (message.Length > 0)
|
||||
{
|
||||
Buffer.BlockCopy(message, 0, Data, offset, message.Length);
|
||||
}
|
||||
|
||||
pkt.AddRange(Message);
|
||||
|
||||
Data = pkt.ToArray();
|
||||
|
||||
PayloadLength = message.LongLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
public override long Parse(byte[] data, uint offset, uint ends)
|
||||
{
|
||||
ValidateBounds(data, offset, ends);
|
||||
var originalOffset = offset;
|
||||
|
||||
if (TryGetMissingBytes(offset, ends, 2, out var incomplete))
|
||||
return incomplete;
|
||||
|
||||
var first = data[offset++];
|
||||
var second = data[offset++];
|
||||
|
||||
FIN = (first & 0x80) != 0;
|
||||
RSV1 = (first & 0x40) != 0;
|
||||
RSV2 = (first & 0x20) != 0;
|
||||
RSV3 = (first & 0x10) != 0;
|
||||
Opcode = (WSOpcode)(first & 0x0F);
|
||||
Mask = (second & 0x80) != 0;
|
||||
|
||||
if (ExpectedMask.HasValue && Mask != ExpectedMask.Value)
|
||||
throw new InvalidDataException(ExpectedMask.Value
|
||||
? "WebSocket clients must mask every frame sent to a server."
|
||||
: "WebSocket servers must not mask frames sent to a client.");
|
||||
|
||||
if (RSV1 || RSV2 || RSV3)
|
||||
throw new InvalidDataException("WebSocket extensions are not enabled for this connection.");
|
||||
|
||||
ulong payloadLength = (byte)(second & 0x7F);
|
||||
if (payloadLength == 126)
|
||||
{
|
||||
if (TryGetMissingBytes(offset, ends, 2, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
payloadLength = (uint)(data[offset] << 8 | data[offset + 1]);
|
||||
offset += 2;
|
||||
|
||||
if (payloadLength < 126)
|
||||
throw new InvalidDataException("WebSocket payload length is not minimally encoded.");
|
||||
}
|
||||
else if (payloadLength == 127)
|
||||
{
|
||||
if (TryGetMissingBytes(offset, ends, 8, out incomplete))
|
||||
return incomplete;
|
||||
if ((data[offset] & 0x80) != 0)
|
||||
throw new InvalidDataException("WebSocket payload length exceeds the protocol limit.");
|
||||
|
||||
payloadLength = 0;
|
||||
for (var i = 0; i < 8; i++)
|
||||
payloadLength = payloadLength << 8 | data[offset++];
|
||||
|
||||
if (payloadLength <= ushort.MaxValue)
|
||||
throw new InvalidDataException("WebSocket payload length is not minimally encoded.");
|
||||
}
|
||||
|
||||
ValidateFrame(Opcode, FIN, payloadLength);
|
||||
EnsureWithinLimit(payloadLength);
|
||||
if (payloadLength > int.MaxValue)
|
||||
throw new ParserLimitException("WebSocket payload cannot fit in a managed byte array.");
|
||||
|
||||
if (Mask)
|
||||
{
|
||||
if (TryGetMissingBytes(offset, ends, 4, out incomplete))
|
||||
return incomplete;
|
||||
|
||||
MaskKey = new byte[4];
|
||||
Buffer.BlockCopy(data, (int)offset, MaskKey, 0, MaskKey.Length);
|
||||
offset += 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
MaskKey = null;
|
||||
}
|
||||
|
||||
var availablePayload = ends - offset;
|
||||
if ((ulong)availablePayload < payloadLength)
|
||||
return -(long)(payloadLength - availablePayload);
|
||||
|
||||
Message = new byte[(int)payloadLength];
|
||||
if (Mask)
|
||||
{
|
||||
for (var i = 0; i < Message.Length; i++)
|
||||
Message[i] = (byte)(data[offset + i] ^ MaskKey[i & 3]);
|
||||
}
|
||||
else if (Message.Length > 0)
|
||||
{
|
||||
Buffer.BlockCopy(data, (int)offset, Message, 0, Message.Length);
|
||||
}
|
||||
|
||||
offset += (uint)payloadLength;
|
||||
PayloadLength = (long)payloadLength;
|
||||
ValidateApplicationPayload(Opcode, FIN, Message);
|
||||
return offset - originalOffset;
|
||||
}
|
||||
|
||||
private void EnsureWithinLimit(ulong payloadLength)
|
||||
{
|
||||
if (MaximumPayloadLength > 0 && payloadLength > MaximumPayloadLength)
|
||||
throw new ParserLimitException(
|
||||
$"WebSocket payload of {payloadLength} bytes exceeds the {MaximumPayloadLength}-byte limit.");
|
||||
}
|
||||
|
||||
private static void ValidateFrame(WSOpcode opcode, bool final, ulong payloadLength)
|
||||
{
|
||||
var isControl = opcode == WSOpcode.ConnectionClose ||
|
||||
opcode == WSOpcode.Ping ||
|
||||
opcode == WSOpcode.Pong;
|
||||
var isData = opcode == WSOpcode.ContinuationFrame ||
|
||||
opcode == WSOpcode.TextFrame ||
|
||||
opcode == WSOpcode.BinaryFrame;
|
||||
|
||||
if (!isControl && !isData)
|
||||
throw new InvalidDataException($"Unsupported WebSocket opcode: 0x{(byte)opcode:X}.");
|
||||
if (isControl && (!final || payloadLength > 125))
|
||||
throw new InvalidDataException("WebSocket control frames must be final and at most 125 bytes.");
|
||||
if (opcode == WSOpcode.ConnectionClose && payloadLength == 1)
|
||||
throw new InvalidDataException("A WebSocket close frame cannot contain a one-byte payload.");
|
||||
}
|
||||
|
||||
private static void ValidateApplicationPayload(WSOpcode opcode, bool final, byte[] payload)
|
||||
{
|
||||
if (opcode == WSOpcode.TextFrame && final)
|
||||
ValidateTextPayload(payload);
|
||||
else if (opcode == WSOpcode.ConnectionClose)
|
||||
ValidateClosePayload(payload);
|
||||
}
|
||||
|
||||
internal static void ValidateTextPayload(byte[] payload)
|
||||
=> ValidateTextPayload(payload ?? Array.Empty<byte>(), 0, payload?.Length ?? 0);
|
||||
|
||||
private static void ValidateTextPayload(byte[] payload, int offset, int count)
|
||||
{
|
||||
try
|
||||
{
|
||||
long needed = 2;
|
||||
var length = ends - offset;
|
||||
if (length < needed)
|
||||
{
|
||||
//Console.WriteLine("stage 1 " + needed);
|
||||
return length - needed;
|
||||
}
|
||||
|
||||
uint oOffset = offset;
|
||||
FIN = (data[offset] & 0x80) == 0x80;
|
||||
RSV1 = (data[offset] & 0x40) == 0x40;
|
||||
RSV2 = (data[offset] & 0x20) == 0x20;
|
||||
RSV3 = (data[offset] & 0x10) == 0x10;
|
||||
Opcode = (WSOpcode)(data[offset++] & 0xF);
|
||||
Mask = (data[offset] & 0x80) == 0x80;
|
||||
PayloadLength = data[offset++] & 0x7F;
|
||||
|
||||
if (Mask)
|
||||
needed += 4;
|
||||
|
||||
if (PayloadLength == 126)
|
||||
{
|
||||
needed += 2;
|
||||
if (length < needed)
|
||||
{
|
||||
//Console.WriteLine("stage 2 " + needed);
|
||||
return length - needed;
|
||||
}
|
||||
PayloadLength = data.GetUInt16(offset, Endian.Big);
|
||||
offset += 2;
|
||||
}
|
||||
else if (PayloadLength == 127)
|
||||
{
|
||||
needed += 8;
|
||||
if (length < needed)
|
||||
{
|
||||
//Console.WriteLine("stage 3 " + needed);
|
||||
return length - needed;
|
||||
}
|
||||
|
||||
PayloadLength = data.GetInt64(offset, Endian.Big);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
/*
|
||||
if (Mask)
|
||||
{
|
||||
MaskKey = new byte[4];
|
||||
MaskKey[0] = data[offset++];
|
||||
MaskKey[1] = data[offset++];
|
||||
MaskKey[2] = data[offset++];
|
||||
MaskKey[3] = data[offset++];
|
||||
|
||||
//MaskKey = DC.GetUInt32(data, offset);
|
||||
//offset += 4;
|
||||
}
|
||||
*/
|
||||
|
||||
needed += PayloadLength;
|
||||
if (length < needed)
|
||||
{
|
||||
//Console.WriteLine("stage 4");
|
||||
return length - needed;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if (Mask)
|
||||
{
|
||||
MaskKey = new byte[4];
|
||||
MaskKey[0] = data[offset++];
|
||||
MaskKey[1] = data[offset++];
|
||||
MaskKey[2] = data[offset++];
|
||||
MaskKey[3] = data[offset++];
|
||||
|
||||
Message = data.Clip(offset, (uint)PayloadLength);
|
||||
|
||||
//var aMask = BitConverter.GetBytes(MaskKey);
|
||||
for (int i = 0; i < Message.Length; i++)
|
||||
Message[i] = (byte)(Message[i] ^ MaskKey[i % 4]);
|
||||
}
|
||||
else
|
||||
Message = data.Clip(offset, (uint)PayloadLength);
|
||||
|
||||
|
||||
return offset - oOffset + (int)PayloadLength;
|
||||
}
|
||||
_ = StrictUtf8.GetCharCount(payload, offset, count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (DecoderFallbackException exception)
|
||||
{
|
||||
Global.Log(ex);
|
||||
Global.Log("WebsocketPacket", Core.LogType.Debug, offset + "::" + data.ToHex());
|
||||
throw ex;
|
||||
throw new InvalidDataException("WebSocket text payload is not valid UTF-8.", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateClosePayload(byte[] payload)
|
||||
{
|
||||
if (payload == null || payload.Length < 2)
|
||||
return;
|
||||
|
||||
var statusCode = payload[0] << 8 | payload[1];
|
||||
var isDefinedProtocolCode = statusCode >= 1000 && statusCode <= 1014
|
||||
&& statusCode != 1004
|
||||
&& statusCode != 1005
|
||||
&& statusCode != 1006;
|
||||
var isApplicationCode = statusCode >= 3000 && statusCode <= 4999;
|
||||
|
||||
if (!isDefinedProtocolCode && !isApplicationCode)
|
||||
throw new InvalidDataException($"Invalid WebSocket close status code: {statusCode}.");
|
||||
|
||||
if (payload.Length > 2)
|
||||
ValidateTextPayload(payload, 2, payload.Length - 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,12 @@ namespace Esiur.Net.Sockets;
|
||||
public class SSLSocket : ISocket
|
||||
{
|
||||
|
||||
private sealed class PendingSend
|
||||
{
|
||||
public AsyncReply<bool> Reply;
|
||||
public byte[] Buffer;
|
||||
}
|
||||
|
||||
public INetworkReceiver<ISocket> Receiver { get; set; }
|
||||
|
||||
Socket sock;
|
||||
@@ -54,7 +60,10 @@ public class SSLSocket : ISocket
|
||||
|
||||
readonly object sendLock = new object();
|
||||
|
||||
Queue<KeyValuePair<AsyncReply<bool>, byte[]>> sendBufferQueue = new Queue<KeyValuePair<AsyncReply<bool>, byte[]>>();// Queue<byte[]>();
|
||||
readonly Queue<PendingSend> sendBufferQueue = new Queue<PendingSend>();
|
||||
PendingSend currentSend;
|
||||
long pendingSendBytes;
|
||||
long maximumPendingSendBytes = 16 * 1024 * 1024;
|
||||
|
||||
bool asyncSending;
|
||||
bool began = false;
|
||||
@@ -72,33 +81,50 @@ public class SSLSocket : ISocket
|
||||
bool server;
|
||||
string hostname;
|
||||
|
||||
public long PendingSendBytes => Interlocked.Read(ref pendingSendBytes);
|
||||
|
||||
/// <summary>Maximum number of unsent plaintext bytes retained for a slow TLS peer.</summary>
|
||||
public long MaximumPendingSendBytes
|
||||
{
|
||||
get => Interlocked.Read(ref maximumPendingSendBytes);
|
||||
set
|
||||
{
|
||||
if (value <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
Interlocked.Exchange(ref maximumPendingSendBytes, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async AsyncReply<bool> Connect(string hostname, ushort port)
|
||||
{
|
||||
var rt = new AsyncReply<bool>();
|
||||
|
||||
this.hostname = hostname;
|
||||
this.server = false;
|
||||
|
||||
state = SocketState.Connecting;
|
||||
await sock.ConnectAsync(hostname, port);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
await BeginAsync();
|
||||
await sock.ConnectAsync(hostname, port);
|
||||
ssl = new SslStream(new NetworkStream(sock));
|
||||
state = SocketState.Established;
|
||||
|
||||
if (!await BeginAsync())
|
||||
{
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
//OnConnect?.Invoke();
|
||||
Receiver?.NetworkConnect(this);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
state = SocketState.Closed;// .Terminated;
|
||||
Close();
|
||||
Global.Log(ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//private void DataSent(Task task)
|
||||
@@ -132,64 +158,55 @@ public class SSLSocket : ISocket
|
||||
//}
|
||||
|
||||
|
||||
private void SendCallback(IAsyncResult ar)
|
||||
private async Task ProcessSendQueueAsync()
|
||||
{
|
||||
if (ar != null)
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
PendingSend pending;
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
ssl.EndWrite(ar);
|
||||
|
||||
if (ar.AsyncState != null)
|
||||
((AsyncReply<bool>)ar.AsyncState).Trigger(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (state != SocketState.Closed && !sock.Connected)
|
||||
{
|
||||
//state = SocketState.Closed;//.Terminated;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
|
||||
if (sendBufferQueue.Count > 0)
|
||||
{
|
||||
var kv = sendBufferQueue.Dequeue();
|
||||
|
||||
try
|
||||
{
|
||||
ssl.BeginWrite(kv.Value, 0, kv.Value.Length, SendCallback, kv.Key);
|
||||
}
|
||||
catch //(Exception ex)
|
||||
if (held || state == SocketState.Closed || sendBufferQueue.Count == 0)
|
||||
{
|
||||
asyncSending = false;
|
||||
try
|
||||
{
|
||||
if (kv.Key != null)
|
||||
kv.Key.Trigger(false);
|
||||
|
||||
if (state != SocketState.Closed && !sock.Connected)
|
||||
{
|
||||
//state = SocketState.Terminated;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
catch //(Exception ex2)
|
||||
{
|
||||
//state = SocketState.Closed;// .Terminated;
|
||||
Close();
|
||||
}
|
||||
|
||||
//Global.Log("TCPSocket", LogType.Error, ex.ToString());
|
||||
return;
|
||||
}
|
||||
|
||||
pending = sendBufferQueue.Dequeue();
|
||||
currentSend = pending;
|
||||
}
|
||||
else
|
||||
|
||||
try
|
||||
{
|
||||
asyncSending = false;
|
||||
await ssl.WriteAsync(pending.Buffer, 0, pending.Buffer.Length).ConfigureAwait(false);
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
if (ReferenceEquals(currentSend, pending))
|
||||
{
|
||||
currentSend = null;
|
||||
Interlocked.Add(ref pendingSendBytes, -pending.Buffer.Length);
|
||||
}
|
||||
}
|
||||
|
||||
TryCompleteSend(pending, true, null);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
lock (sendLock)
|
||||
{
|
||||
if (ReferenceEquals(currentSend, pending))
|
||||
{
|
||||
currentSend = null;
|
||||
Interlocked.Add(ref pendingSendBytes, -pending.Buffer.Length);
|
||||
}
|
||||
|
||||
asyncSending = false;
|
||||
}
|
||||
|
||||
TryCompleteSend(pending, false, exception);
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -257,25 +274,52 @@ public class SSLSocket : ISocket
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (state != SocketState.Closed)// && state != SocketState.Terminated)
|
||||
List<PendingSend> abandoned;
|
||||
lock (sendLock)
|
||||
{
|
||||
state = SocketState.Closed;
|
||||
if (state == SocketState.Closed)
|
||||
return;
|
||||
|
||||
if (sock.Connected)
|
||||
state = SocketState.Closed;
|
||||
abandoned = sendBufferQueue.ToList();
|
||||
sendBufferQueue.Clear();
|
||||
if (currentSend != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch
|
||||
{
|
||||
//state = SocketState.Terminated;
|
||||
}
|
||||
abandoned.Insert(0, currentSend);
|
||||
currentSend = null;
|
||||
}
|
||||
|
||||
Receiver?.NetworkClose(this);
|
||||
//OnClose?.Invoke();
|
||||
foreach (var pending in abandoned)
|
||||
Interlocked.Add(ref pendingSendBytes, -pending.Buffer.Length);
|
||||
asyncSending = false;
|
||||
}
|
||||
|
||||
if (sock != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (sock.Connected)
|
||||
sock.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
catch
|
||||
{
|
||||
//state = SocketState.Terminated;
|
||||
}
|
||||
|
||||
// Closing the underlying socket is what aborts an in-progress TLS
|
||||
// handshake. Shutdown alone can leave AuthenticateAsServerAsync waiting
|
||||
// forever for a peer that never sends a ClientHello.
|
||||
try { sock.Close(); } catch { }
|
||||
}
|
||||
|
||||
try { ssl?.Dispose(); } catch { }
|
||||
|
||||
foreach (var pending in abandoned)
|
||||
TryCompleteSend(pending, false, null);
|
||||
|
||||
try { Receiver?.NetworkClose(this); }
|
||||
catch (Exception exception) { Global.Log(exception); }
|
||||
//OnClose?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
@@ -287,35 +331,26 @@ public class SSLSocket : ISocket
|
||||
|
||||
public void Send(byte[] message, int offset, int size)
|
||||
{
|
||||
ValidateRange(message, offset, size);
|
||||
if (size == 0)
|
||||
return;
|
||||
|
||||
|
||||
var msg = message.Clip((uint)offset, (uint)size);
|
||||
|
||||
bool startPump = false;
|
||||
lock (sendLock)
|
||||
{
|
||||
|
||||
if (!sock.Connected)
|
||||
if (state != SocketState.Established)
|
||||
return;
|
||||
|
||||
if (asyncSending || held)
|
||||
{
|
||||
sendBufferQueue.Enqueue(new KeyValuePair<AsyncReply<bool>, byte[]>(null, msg));// message.Clip((uint)offset, (uint)size));
|
||||
}
|
||||
else
|
||||
{
|
||||
asyncSending = true;
|
||||
try
|
||||
{
|
||||
ssl.BeginWrite(msg, 0, msg.Length, SendCallback, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
asyncSending = false;
|
||||
//state = SocketState.Terminated;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
EnsureSendCapacity_NoLock(size);
|
||||
var msg = new byte[size];
|
||||
Buffer.BlockCopy(message, offset, msg, 0, size);
|
||||
sendBufferQueue.Enqueue(new PendingSend { Buffer = msg });
|
||||
Interlocked.Add(ref pendingSendBytes, size);
|
||||
startPump = TryStartSendPump_NoLock();
|
||||
}
|
||||
|
||||
if (startPump)
|
||||
_ = ProcessSendQueueAsync();
|
||||
}
|
||||
|
||||
//public void Send(byte[] message)
|
||||
@@ -438,15 +473,16 @@ public class SSLSocket : ISocket
|
||||
ssl.BeginRead(receiveBuffer, 0, receiveBuffer.Length, ReceiveCallback, this);
|
||||
|
||||
}
|
||||
catch //(Exception ex)
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (state != SocketState.Closed && !sock.Connected)
|
||||
{
|
||||
//state = SocketState.Terminated;
|
||||
// Socket.Connected reports the state of the last operation and can
|
||||
// remain true after a TLS read failure. Any read exception ends this
|
||||
// receive loop, so close deterministically instead of leaving a
|
||||
// half-open connection that will never read again.
|
||||
if (state != SocketState.Closed)
|
||||
Close();
|
||||
}
|
||||
|
||||
//Global.Log("SSLSocket", LogType.Error, ex.ToString());
|
||||
Global.Log("SSLSocket", LogType.Warning, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,59 +525,101 @@ public class SSLSocket : ISocket
|
||||
|
||||
public void Hold()
|
||||
{
|
||||
held = true;
|
||||
lock (sendLock)
|
||||
held = true;
|
||||
}
|
||||
|
||||
public void Unhold()
|
||||
{
|
||||
try
|
||||
{
|
||||
SendCallback(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Global.Log(ex);
|
||||
}
|
||||
finally
|
||||
bool startPump;
|
||||
lock (sendLock)
|
||||
{
|
||||
held = false;
|
||||
startPump = TryStartSendPump_NoLock();
|
||||
}
|
||||
|
||||
if (startPump)
|
||||
_ = ProcessSendQueueAsync();
|
||||
}
|
||||
|
||||
|
||||
public AsyncReply<bool> SendAsync(byte[] message, int offset, int length)
|
||||
{
|
||||
ValidateRange(message, offset, length);
|
||||
if (length == 0)
|
||||
return new AsyncReply<bool>(true);
|
||||
|
||||
var msg = message.Clip((uint)offset, (uint)length);
|
||||
|
||||
var rt = new AsyncReply<bool>();
|
||||
bool startPump = false;
|
||||
Exception capacityError = null;
|
||||
lock (sendLock)
|
||||
{
|
||||
if (!sock.Connected)
|
||||
if (state != SocketState.Established)
|
||||
return new AsyncReply<bool>(false);
|
||||
|
||||
var rt = new AsyncReply<bool>();
|
||||
|
||||
if (asyncSending || held)
|
||||
try
|
||||
{
|
||||
sendBufferQueue.Enqueue(new KeyValuePair<AsyncReply<bool>, byte[]>(rt, msg));
|
||||
EnsureSendCapacity_NoLock(length);
|
||||
var msg = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, msg, 0, length);
|
||||
sendBufferQueue.Enqueue(new PendingSend { Reply = rt, Buffer = msg });
|
||||
Interlocked.Add(ref pendingSendBytes, length);
|
||||
startPump = TryStartSendPump_NoLock();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
capacityError = exception;
|
||||
}
|
||||
}
|
||||
|
||||
if (capacityError != null)
|
||||
rt.TriggerError(capacityError);
|
||||
else if (startPump)
|
||||
_ = ProcessSendQueueAsync();
|
||||
|
||||
return rt;
|
||||
}
|
||||
|
||||
private bool TryStartSendPump_NoLock()
|
||||
{
|
||||
if (asyncSending || held || state != SocketState.Established || sendBufferQueue.Count == 0)
|
||||
return false;
|
||||
|
||||
asyncSending = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void EnsureSendCapacity_NoLock(int length)
|
||||
{
|
||||
var limit = Interlocked.Read(ref maximumPendingSendBytes);
|
||||
var pending = Interlocked.Read(ref pendingSendBytes);
|
||||
if (length > limit - pending)
|
||||
throw new InvalidOperationException($"The TLS send queue exceeded its {limit}-byte limit.");
|
||||
}
|
||||
|
||||
private static void ValidateRange(byte[] message, int offset, int length)
|
||||
{
|
||||
if (message == null)
|
||||
throw new ArgumentNullException(nameof(message));
|
||||
if (offset < 0 || length < 0 || offset > message.Length - length)
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
private static void TryCompleteSend(PendingSend pending, bool succeeded, Exception exception)
|
||||
{
|
||||
if (pending?.Reply == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (exception != null)
|
||||
pending.Reply.TriggerError(exception);
|
||||
else
|
||||
{
|
||||
asyncSending = true;
|
||||
try
|
||||
{
|
||||
ssl.BeginWrite(msg, 0, msg.Length, SendCallback, rt);// null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
rt.TriggerError(ex);
|
||||
asyncSending = false;
|
||||
//state = SocketState.Terminated;
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
return rt;
|
||||
pending.Reply.Trigger(succeeded);
|
||||
}
|
||||
catch (Exception callbackException)
|
||||
{
|
||||
Global.Log(callbackException);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,20 @@ public class TcpSocket : ISocket
|
||||
public AsyncReply<bool> Reply;
|
||||
}
|
||||
|
||||
private readonly struct SendReplyCompletion
|
||||
{
|
||||
public SendReplyCompletion(AsyncReply<bool> reply, bool result, Exception error)
|
||||
{
|
||||
Reply = reply;
|
||||
Result = result;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public AsyncReply<bool> Reply { get; }
|
||||
public bool Result { get; }
|
||||
public Exception Error { get; }
|
||||
}
|
||||
|
||||
public INetworkReceiver<ISocket> Receiver { get; set; }
|
||||
public event DestroyedEvent OnDestroy;
|
||||
|
||||
@@ -37,6 +51,8 @@ public class TcpSocket : ISocket
|
||||
private SocketAsyncEventArgs sendArgs;
|
||||
|
||||
private PendingSend currentSend;
|
||||
private long pendingSendBytes;
|
||||
private long maximumPendingSendBytes = 16 * 1024 * 1024;
|
||||
private bool sendInProgress;
|
||||
private bool began;
|
||||
private bool held;
|
||||
@@ -52,6 +68,24 @@ public class TcpSocket : ISocket
|
||||
public SocketState State => state;
|
||||
public int BytesSent => bytesSent;
|
||||
public int BytesReceived => bytesReceived;
|
||||
public long PendingSendBytes => Interlocked.Read(ref pendingSendBytes);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of unsent bytes retained by this socket. This bounds the
|
||||
/// copies made by <see cref="Send(byte[], int, int)"/> and
|
||||
/// <see cref="SendAsync(byte[], int, int)"/> when a peer is slow.
|
||||
/// </summary>
|
||||
public long MaximumPendingSendBytes
|
||||
{
|
||||
get => Interlocked.Read(ref maximumPendingSendBytes);
|
||||
set
|
||||
{
|
||||
if (value <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
|
||||
Interlocked.Exchange(ref maximumPendingSendBytes, value);
|
||||
}
|
||||
}
|
||||
|
||||
public IPEndPoint LocalEndPoint => sock.LocalEndPoint as IPEndPoint;
|
||||
public IPEndPoint RemoteEndPoint => sock.RemoteEndPoint as IPEndPoint;
|
||||
@@ -227,14 +261,19 @@ public class TcpSocket : ISocket
|
||||
if (destroyed || state != SocketState.Established)
|
||||
return;
|
||||
|
||||
var copy = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, copy, 0, length);
|
||||
List<SendReplyCompletion> completions = null;
|
||||
Exception sendError = null;
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
if (destroyed || state != SocketState.Established)
|
||||
return;
|
||||
|
||||
EnsureSendCapacity_NoLock(length);
|
||||
|
||||
var copy = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, copy, 0, length);
|
||||
|
||||
sendQueue.Enqueue(new PendingSend
|
||||
{
|
||||
Buffer = copy,
|
||||
@@ -242,9 +281,12 @@ public class TcpSocket : ISocket
|
||||
Count = copy.Length,
|
||||
Reply = null
|
||||
});
|
||||
Interlocked.Add(ref pendingSendBytes, length);
|
||||
|
||||
TryStartNextSend_NoLock();
|
||||
TryStartNextSend_NoLock(ref completions, ref sendError);
|
||||
}
|
||||
|
||||
FinishSendWork(completions, sendError);
|
||||
}
|
||||
|
||||
public AsyncReply<bool> SendAsync(byte[] message, int offset, int length)
|
||||
@@ -268,8 +310,8 @@ public class TcpSocket : ISocket
|
||||
return rt;
|
||||
}
|
||||
|
||||
var copy = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, copy, 0, length);
|
||||
List<SendReplyCompletion> completions = null;
|
||||
Exception sendError = null;
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
@@ -279,6 +321,19 @@ public class TcpSocket : ISocket
|
||||
return rt;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EnsureSendCapacity_NoLock(length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
rt.TriggerError(ex);
|
||||
return rt;
|
||||
}
|
||||
|
||||
var copy = new byte[length];
|
||||
Buffer.BlockCopy(message, offset, copy, 0, length);
|
||||
|
||||
sendQueue.Enqueue(new PendingSend
|
||||
{
|
||||
Buffer = copy,
|
||||
@@ -286,10 +341,13 @@ public class TcpSocket : ISocket
|
||||
Count = copy.Length,
|
||||
Reply = rt
|
||||
});
|
||||
Interlocked.Add(ref pendingSendBytes, length);
|
||||
|
||||
TryStartNextSend_NoLock();
|
||||
TryStartNextSend_NoLock(ref completions, ref sendError);
|
||||
}
|
||||
|
||||
FinishSendWork(completions, sendError);
|
||||
|
||||
return rt;
|
||||
}
|
||||
|
||||
@@ -323,15 +381,22 @@ public class TcpSocket : ISocket
|
||||
|
||||
public void Hold()
|
||||
{
|
||||
held = true;
|
||||
lock (sendLock)
|
||||
held = true;
|
||||
}
|
||||
|
||||
public void Unhold()
|
||||
{
|
||||
held = false;
|
||||
List<SendReplyCompletion> completions = null;
|
||||
Exception sendError = null;
|
||||
|
||||
lock (sendLock)
|
||||
TryStartNextSend_NoLock();
|
||||
{
|
||||
held = false;
|
||||
TryStartNextSend_NoLock(ref completions, ref sendError);
|
||||
}
|
||||
|
||||
FinishSendWork(completions, sendError);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
@@ -444,16 +509,20 @@ public class TcpSocket : ISocket
|
||||
}
|
||||
}
|
||||
|
||||
private void TryStartNextSend_NoLock()
|
||||
private void TryStartNextSend_NoLock(
|
||||
ref List<SendReplyCompletion> completions,
|
||||
ref Exception sendError)
|
||||
{
|
||||
if (held || destroyed || state != SocketState.Established || sendInProgress)
|
||||
return;
|
||||
|
||||
sendInProgress = true;
|
||||
PumpSendQueue_NoLock();
|
||||
PumpSendQueue_NoLock(ref completions, ref sendError);
|
||||
}
|
||||
|
||||
private void PumpSendQueue_NoLock()
|
||||
private void PumpSendQueue_NoLock(
|
||||
ref List<SendReplyCompletion> completions,
|
||||
ref Exception sendError)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
@@ -492,9 +561,9 @@ public class TcpSocket : ISocket
|
||||
var reply = currentSend?.Reply;
|
||||
currentSend = null;
|
||||
sendInProgress = false;
|
||||
reply?.TriggerError(ex);
|
||||
FailPendingSends_NoLock(ex);
|
||||
CloseDueToSendError_NoLock(ex);
|
||||
QueueSendCompletion(ref completions, reply, false, ex);
|
||||
FailPendingSends_NoLock(ex, ref completions);
|
||||
sendError = ex;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -503,23 +572,35 @@ public class TcpSocket : ISocket
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ProcessSendCompletion_NoLock(sendArgs))
|
||||
if (!ProcessSendCompletion_NoLock(
|
||||
sendArgs,
|
||||
ref completions,
|
||||
ref sendError))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessSend(SocketAsyncEventArgs e)
|
||||
{
|
||||
List<SendReplyCompletion> completions = null;
|
||||
Exception sendError = null;
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
if (!ProcessSendCompletion_NoLock(e))
|
||||
return;
|
||||
|
||||
PumpSendQueue_NoLock();
|
||||
if (ProcessSendCompletion_NoLock(
|
||||
e,
|
||||
ref completions,
|
||||
ref sendError))
|
||||
PumpSendQueue_NoLock(ref completions, ref sendError);
|
||||
}
|
||||
|
||||
FinishSendWork(completions, sendError);
|
||||
}
|
||||
|
||||
private bool ProcessSendCompletion_NoLock(SocketAsyncEventArgs e)
|
||||
private bool ProcessSendCompletion_NoLock(
|
||||
SocketAsyncEventArgs e,
|
||||
ref List<SendReplyCompletion> completions,
|
||||
ref Exception sendError)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -532,26 +613,27 @@ public class TcpSocket : ISocket
|
||||
if (e.SocketError != SocketError.Success)
|
||||
{
|
||||
var ex = new SocketException((int)e.SocketError);
|
||||
currentSend.Reply?.TriggerError(ex);
|
||||
QueueSendCompletion(ref completions, currentSend.Reply, false, ex);
|
||||
currentSend = null;
|
||||
sendInProgress = false;
|
||||
FailPendingSends_NoLock(ex);
|
||||
CloseDueToSendError_NoLock(ex);
|
||||
FailPendingSends_NoLock(ex, ref completions);
|
||||
sendError = ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.BytesTransferred <= 0)
|
||||
{
|
||||
var ex = new SocketException((int)SocketError.ConnectionReset);
|
||||
currentSend.Reply?.TriggerError(ex);
|
||||
QueueSendCompletion(ref completions, currentSend.Reply, false, ex);
|
||||
currentSend = null;
|
||||
sendInProgress = false;
|
||||
FailPendingSends_NoLock(ex);
|
||||
CloseDueToSendError_NoLock(ex);
|
||||
FailPendingSends_NoLock(ex, ref completions);
|
||||
sendError = ex;
|
||||
return false;
|
||||
}
|
||||
|
||||
Interlocked.Add(ref bytesSent, e.BytesTransferred);
|
||||
Interlocked.Add(ref pendingSendBytes, -e.BytesTransferred);
|
||||
|
||||
currentSend.Offset += e.BytesTransferred;
|
||||
currentSend.Count -= e.BytesTransferred;
|
||||
@@ -561,42 +643,42 @@ public class TcpSocket : ISocket
|
||||
return true;
|
||||
}
|
||||
|
||||
currentSend.Reply?.Trigger(true);
|
||||
QueueSendCompletion(ref completions, currentSend.Reply, true, null);
|
||||
currentSend = null;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
currentSend?.Reply?.TriggerError(ex);
|
||||
QueueSendCompletion(ref completions, currentSend?.Reply, false, ex);
|
||||
currentSend = null;
|
||||
sendInProgress = false;
|
||||
FailPendingSends_NoLock(ex);
|
||||
CloseDueToSendError_NoLock(ex);
|
||||
FailPendingSends_NoLock(ex, ref completions);
|
||||
sendError = ex;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void FailPendingSends_NoLock(Exception ex)
|
||||
private void FailPendingSends_NoLock(
|
||||
Exception ex,
|
||||
ref List<SendReplyCompletion> completions)
|
||||
{
|
||||
while (sendQueue.Count > 0)
|
||||
{
|
||||
var item = sendQueue.Dequeue();
|
||||
try
|
||||
{
|
||||
item.Reply?.TriggerError(ex);
|
||||
}
|
||||
catch { }
|
||||
QueueSendCompletion(ref completions, item.Reply, false, ex);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref pendingSendBytes, 0);
|
||||
}
|
||||
|
||||
private void CloseDueToSendError_NoLock(Exception ex)
|
||||
private bool CloseDueToSendError(Exception ex)
|
||||
{
|
||||
bool notify = false;
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
if (state == SocketState.Closed)
|
||||
return;
|
||||
return false;
|
||||
|
||||
state = SocketState.Closed;
|
||||
notify = !closeNotified;
|
||||
@@ -609,6 +691,17 @@ public class TcpSocket : ISocket
|
||||
|
||||
Global.Log(ex);
|
||||
|
||||
return notify;
|
||||
}
|
||||
|
||||
private void FinishSendWork(
|
||||
List<SendReplyCompletion> completions,
|
||||
Exception sendError)
|
||||
{
|
||||
var notify = sendError != null && CloseDueToSendError(sendError);
|
||||
|
||||
CompleteSendReplies(completions);
|
||||
|
||||
if (notify)
|
||||
{
|
||||
try { Receiver?.NetworkClose(this); }
|
||||
@@ -616,9 +709,44 @@ public class TcpSocket : ISocket
|
||||
}
|
||||
}
|
||||
|
||||
private static void QueueSendCompletion(
|
||||
ref List<SendReplyCompletion> completions,
|
||||
AsyncReply<bool> reply,
|
||||
bool result,
|
||||
Exception error)
|
||||
{
|
||||
if (reply == null)
|
||||
return;
|
||||
|
||||
completions ??= new List<SendReplyCompletion>();
|
||||
completions.Add(new SendReplyCompletion(reply, result, error));
|
||||
}
|
||||
|
||||
private static void CompleteSendReplies(List<SendReplyCompletion> completions)
|
||||
{
|
||||
if (completions == null)
|
||||
return;
|
||||
|
||||
foreach (var completion in completions)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (completion.Error != null)
|
||||
completion.Reply.TriggerError(completion.Error);
|
||||
else
|
||||
completion.Reply.Trigger(completion.Result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Global.Log(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SafeClose(Exception ex, bool notifyReceiver)
|
||||
{
|
||||
bool notify = false;
|
||||
List<SendReplyCompletion> completions = null;
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
@@ -636,25 +764,30 @@ public class TcpSocket : ISocket
|
||||
|
||||
if (ex != null)
|
||||
{
|
||||
try { currentSend?.Reply?.TriggerError(ex); } catch { }
|
||||
QueueSendCompletion(ref completions, currentSend?.Reply, false, ex);
|
||||
currentSend = null;
|
||||
FailPendingSends_NoLock(ex);
|
||||
FailPendingSends_NoLock(ex, ref completions);
|
||||
}
|
||||
else
|
||||
{
|
||||
QueueSendCompletion(ref completions, currentSend?.Reply, false, null);
|
||||
currentSend = null;
|
||||
while (sendQueue.Count > 0)
|
||||
{
|
||||
var item = sendQueue.Dequeue();
|
||||
try { item.Reply?.Trigger(false); } catch { }
|
||||
QueueSendCompletion(ref completions, item.Reply, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref pendingSendBytes, 0);
|
||||
}
|
||||
|
||||
try { sock.Shutdown(SocketShutdown.Both); } catch { }
|
||||
try { sock.Close(); } catch { }
|
||||
try { sock.Dispose(); } catch { }
|
||||
|
||||
CompleteSendReplies(completions);
|
||||
|
||||
if (ex != null)
|
||||
Global.Log(ex);
|
||||
|
||||
@@ -667,7 +800,19 @@ public class TcpSocket : ISocket
|
||||
|
||||
private static void ValidateRange(byte[] message, int offset, int length)
|
||||
{
|
||||
if (offset < 0 || length < 0 || offset + length > message.Length)
|
||||
if (offset < 0 || length < 0 || offset > message.Length - length)
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureSendCapacity_NoLock(int length)
|
||||
{
|
||||
var limit = Interlocked.Read(ref maximumPendingSendBytes);
|
||||
var pending = Interlocked.Read(ref pendingSendBytes);
|
||||
|
||||
if (length > limit - pending)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The socket send queue exceeded its {limit}-byte limit.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +45,16 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
ISocket sock;
|
||||
NetworkBuffer receiveNetworkBuffer = new NetworkBuffer();
|
||||
NetworkBuffer sendNetworkBuffer = new NetworkBuffer();
|
||||
NetworkBuffer fragmentedMessageBuffer = new NetworkBuffer();
|
||||
|
||||
bool fragmentedMessage;
|
||||
WebsocketPacket.WSOpcode fragmentedMessageOpcode;
|
||||
ulong fragmentedMessageLength;
|
||||
|
||||
object sendLock = new object();
|
||||
bool held;
|
||||
bool destroyed;
|
||||
ulong maximumMessageLength = WebsocketPacket.DefaultMaximumPayloadLength;
|
||||
|
||||
//public event ISocketReceiveEvent OnReceive;
|
||||
//public event ISocketConnectEvent OnConnect;
|
||||
@@ -79,11 +86,32 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
public INetworkReceiver<ISocket> Receiver { get; set; }
|
||||
|
||||
public WSocket(ISocket socket)
|
||||
/// <summary>Whether this endpoint receives client frames and sends server frames.</summary>
|
||||
public bool IsServer { get; }
|
||||
|
||||
/// <summary>Maximum payload accepted for one complete message.</summary>
|
||||
public ulong MaximumMessageLength
|
||||
{
|
||||
get => maximumMessageLength;
|
||||
set
|
||||
{
|
||||
maximumMessageLength = value;
|
||||
pkt_receive.MaximumPayloadLength = value;
|
||||
}
|
||||
}
|
||||
|
||||
public WSocket(ISocket socket)
|
||||
: this(socket, true)
|
||||
{
|
||||
}
|
||||
|
||||
public WSocket(ISocket socket, bool isServer)
|
||||
{
|
||||
IsServer = isServer;
|
||||
pkt_send.FIN = true;
|
||||
pkt_send.Mask = false;
|
||||
pkt_send.Mask = !isServer;
|
||||
pkt_send.Opcode = WebsocketPacket.WSOpcode.BinaryFrame;
|
||||
pkt_receive.ExpectedMask = isServer;
|
||||
sock = socket;
|
||||
|
||||
sock.Receiver = this;
|
||||
@@ -111,8 +139,11 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
public void Send(WebsocketPacket packet)
|
||||
{
|
||||
lock (sendLock)
|
||||
{
|
||||
PrepareOutboundPacket(packet);
|
||||
if (packet.Compose())
|
||||
sock.Send(packet.Data);
|
||||
}
|
||||
}
|
||||
|
||||
public void Send(byte[] message)
|
||||
@@ -131,6 +162,7 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
pkt_send.Message = message;
|
||||
|
||||
PrepareOutboundPacket(pkt_send);
|
||||
if (pkt_send.Compose())
|
||||
sock?.Send(pkt_send.Data);
|
||||
|
||||
@@ -154,6 +186,7 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
pkt_send.Message = new byte[size];
|
||||
Buffer.BlockCopy(message, offset, pkt_send.Message, 0, size);
|
||||
PrepareOutboundPacket(pkt_send);
|
||||
if (pkt_send.Compose())
|
||||
sock.Send(pkt_send.Data);
|
||||
}
|
||||
@@ -184,18 +217,36 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
Close();
|
||||
//OnClose = null;
|
||||
//OnConnect = null;
|
||||
//OnReceive = null;
|
||||
receiveNetworkBuffer = null;
|
||||
//sock.OnReceive -= Sock_OnReceive;
|
||||
//sock.OnClose -= Sock_OnClose;
|
||||
//sock.OnConnect -= Sock_OnConnect;
|
||||
sock.Receiver = null;
|
||||
sock = null;
|
||||
OnDestroy?.Invoke(this);
|
||||
OnDestroy = null;
|
||||
ISocket socket;
|
||||
DestroyedEvent onDestroy;
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
if (destroyed)
|
||||
return;
|
||||
|
||||
destroyed = true;
|
||||
socket = sock;
|
||||
onDestroy = OnDestroy;
|
||||
OnDestroy = null;
|
||||
}
|
||||
|
||||
// Close can synchronously re-enter Destroy through NetworkClose. The
|
||||
// guard above keeps that path idempotent while the captured socket is
|
||||
// still valid for the outer cleanup.
|
||||
try { socket?.Close(); } catch (Exception ex) { Global.Log(ex); }
|
||||
|
||||
lock (sendLock)
|
||||
{
|
||||
if (socket != null && ReferenceEquals(socket.Receiver, this))
|
||||
socket.Receiver = null;
|
||||
|
||||
sock = null;
|
||||
receiveNetworkBuffer = null;
|
||||
fragmentedMessageBuffer = null;
|
||||
}
|
||||
|
||||
onDestroy?.Invoke(this);
|
||||
}
|
||||
|
||||
public AsyncReply<ISocket> AcceptAsync()
|
||||
@@ -205,8 +256,8 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
public void Hold()
|
||||
{
|
||||
//Console.WriteLine("WS Hold ");
|
||||
held = true;
|
||||
lock (sendLock)
|
||||
held = true;
|
||||
}
|
||||
|
||||
public void Unhold()
|
||||
@@ -225,6 +276,7 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
totalSent += message.Length;
|
||||
|
||||
pkt_send.Message = message;
|
||||
PrepareOutboundPacket(pkt_send);
|
||||
if (pkt_send.Compose())
|
||||
sock.Send(pkt_send.Data);
|
||||
|
||||
@@ -248,7 +300,7 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
public void NetworkClose(ISocket sender)
|
||||
{
|
||||
Receiver?.NetworkClose(sender);
|
||||
Receiver?.NetworkClose(this);
|
||||
}
|
||||
|
||||
public void NetworkReceive(ISocket sender, NetworkBuffer buffer)
|
||||
@@ -267,7 +319,8 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
if (msg == null)
|
||||
return;
|
||||
|
||||
var wsPacketLength = pkt_receive.Parse(msg, 0, (uint)msg.Length);
|
||||
if (!TryParseFrame(msg, 0, out var wsPacketLength))
|
||||
return;
|
||||
|
||||
if (wsPacketLength < 0)
|
||||
{
|
||||
@@ -289,45 +342,33 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
var pkt_pong = new WebsocketPacket()
|
||||
{
|
||||
FIN = true,
|
||||
Mask = false,
|
||||
Mask = !IsServer,
|
||||
Opcode = WebsocketPacket.WSOpcode.Pong,
|
||||
Message = pkt_receive.Message
|
||||
};
|
||||
|
||||
offset += (uint)wsPacketLength;
|
||||
|
||||
Send(pkt_pong);
|
||||
}
|
||||
else if (pkt_receive.Opcode == WebsocketPacket.WSOpcode.Pong)
|
||||
{
|
||||
offset += (uint)wsPacketLength;
|
||||
}
|
||||
else if (pkt_receive.Opcode == WebsocketPacket.WSOpcode.BinaryFrame
|
||||
|| pkt_receive.Opcode == WebsocketPacket.WSOpcode.TextFrame
|
||||
|| pkt_receive.Opcode == WebsocketPacket.WSOpcode.ContinuationFrame)
|
||||
{
|
||||
totalReceived += pkt_receive.Message.Length;
|
||||
//Console.WriteLine("RX " + pkt_receive.Message.Length + "/" + totalReceived);// + " " + DC.ToHex(message, 0, (uint)size));
|
||||
|
||||
receiveNetworkBuffer.Write(pkt_receive.Message);
|
||||
offset += (uint)wsPacketLength;
|
||||
|
||||
//Console.WriteLine("WS IN: " + pkt_receive.Opcode.ToString() + " " + pkt_receive.Message.Length + " | " + offset + " " + string.Join(" ", pkt_receive.Message));// DC.ToHex(pkt_receive.Message));
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Global.Log("WSocket", LogType.Debug, "Unknown WS opcode:" + pkt_receive.Opcode);
|
||||
if (!ProcessDataFrame(pkt_receive))
|
||||
return;
|
||||
}
|
||||
|
||||
// Pong frames need no further processing. All successfully handled frames
|
||||
// advance by the same parsed length.
|
||||
offset += (uint)wsPacketLength;
|
||||
|
||||
if (offset == msg.Length)
|
||||
{
|
||||
|
||||
Receiver?.NetworkReceive(this, receiveNetworkBuffer);
|
||||
DeliverReceivedData();
|
||||
return;
|
||||
}
|
||||
|
||||
wsPacketLength = pkt_receive.Parse(msg, offset, (uint)msg.Length);
|
||||
if (!TryParseFrame(msg, offset, out wsPacketLength))
|
||||
return;
|
||||
}
|
||||
|
||||
if (wsPacketLength < 0)
|
||||
@@ -339,13 +380,128 @@ public class WSocket : ISocket, INetworkReceiver<ISocket>
|
||||
|
||||
//Console.WriteLine("WS IN: " + receiveNetworkBuffer.Available);
|
||||
|
||||
Receiver?.NetworkReceive(this, receiveNetworkBuffer);
|
||||
DeliverReceivedData();
|
||||
|
||||
|
||||
if (buffer.Available > 0 && !buffer.Protected)
|
||||
NetworkReceive(this, buffer);
|
||||
}
|
||||
|
||||
private bool ProcessDataFrame(WebsocketPacket packet)
|
||||
{
|
||||
var payload = packet.Message ?? Array.Empty<byte>();
|
||||
|
||||
if (MaximumMessageLength > 0 && (ulong)payload.LongLength > MaximumMessageLength)
|
||||
return RejectProtocol($"WebSocket message exceeds the {MaximumMessageLength}-byte limit.");
|
||||
|
||||
if (packet.Opcode == WebsocketPacket.WSOpcode.TextFrame
|
||||
|| packet.Opcode == WebsocketPacket.WSOpcode.BinaryFrame)
|
||||
{
|
||||
if (fragmentedMessage)
|
||||
return RejectProtocol("A new WebSocket data frame arrived before the fragmented message completed.");
|
||||
|
||||
if (packet.FIN)
|
||||
{
|
||||
receiveNetworkBuffer.Write(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
fragmentedMessage = true;
|
||||
fragmentedMessageOpcode = packet.Opcode;
|
||||
fragmentedMessageLength = 0;
|
||||
fragmentedMessageBuffer.Read();
|
||||
return AppendFragment(payload);
|
||||
}
|
||||
|
||||
if (!fragmentedMessage)
|
||||
return RejectProtocol("A WebSocket continuation frame arrived without an active fragmented message.");
|
||||
|
||||
if (!AppendFragment(payload))
|
||||
return false;
|
||||
|
||||
if (!packet.FIN)
|
||||
return true;
|
||||
|
||||
var message = fragmentedMessageBuffer.Read() ?? Array.Empty<byte>();
|
||||
try
|
||||
{
|
||||
if (fragmentedMessageOpcode == WebsocketPacket.WSOpcode.TextFrame)
|
||||
WebsocketPacket.ValidateTextPayload(message);
|
||||
}
|
||||
catch (InvalidDataException exception)
|
||||
{
|
||||
return RejectProtocol(exception.Message);
|
||||
}
|
||||
|
||||
receiveNetworkBuffer.Write(message);
|
||||
ResetFragmentedMessage();
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool AppendFragment(byte[] payload)
|
||||
{
|
||||
var nextLength = fragmentedMessageLength + (ulong)payload.LongLength;
|
||||
if (nextLength < fragmentedMessageLength
|
||||
|| nextLength > int.MaxValue
|
||||
|| (MaximumMessageLength > 0 && nextLength > MaximumMessageLength))
|
||||
return RejectProtocol($"WebSocket fragmented message exceeds the {MaximumMessageLength}-byte limit.");
|
||||
|
||||
fragmentedMessageBuffer.Write(payload);
|
||||
fragmentedMessageLength = nextLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DeliverReceivedData()
|
||||
{
|
||||
if (receiveNetworkBuffer != null && receiveNetworkBuffer.Available > 0)
|
||||
Receiver?.NetworkReceive(this, receiveNetworkBuffer);
|
||||
}
|
||||
|
||||
private bool RejectProtocol(string message)
|
||||
{
|
||||
Global.Log("WSocket", LogType.Warning, message);
|
||||
ResetFragmentedMessage();
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ResetFragmentedMessage()
|
||||
{
|
||||
fragmentedMessage = false;
|
||||
fragmentedMessageLength = 0;
|
||||
fragmentedMessageOpcode = default;
|
||||
fragmentedMessageBuffer?.Read();
|
||||
}
|
||||
|
||||
private void PrepareOutboundPacket(WebsocketPacket packet)
|
||||
{
|
||||
packet.Mask = !IsServer;
|
||||
|
||||
// RFC 6455 requires a fresh unpredictable masking key for every client
|
||||
// frame. pkt_send is intentionally reused, so discard its previous key.
|
||||
if (packet.Mask)
|
||||
packet.MaskKey = null;
|
||||
}
|
||||
|
||||
private bool TryParseFrame(byte[] message, uint offset, out long packetLength)
|
||||
{
|
||||
try
|
||||
{
|
||||
packetLength = pkt_receive.Parse(message, offset, (uint)message.Length);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is InvalidDataException ||
|
||||
exception is ParserLimitException ||
|
||||
exception is ArgumentException)
|
||||
{
|
||||
Global.Log(exception);
|
||||
packetLength = 0;
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void NetworkConnect(ISocket sender)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user