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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user