mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
359 lines
14 KiB
C#
359 lines
14 KiB
C#
using Esiur.Data;
|
|
using Esiur.Misc;
|
|
using System;
|
|
using System.Globalization;
|
|
using System.Net;
|
|
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;
|
|
public string URL;
|
|
public string Filename;
|
|
public KeyList<string, object> PostForms;
|
|
public byte[] Message;
|
|
|
|
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()
|
|
=> $"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)
|
|
{
|
|
ValidateBounds(data, offset, ends);
|
|
var originalOffset = offset;
|
|
|
|
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;
|
|
|
|
var requestLine = lines[0].Split(new[] { ' ' }, 3, StringSplitOptions.RemoveEmptyEntries);
|
|
if (requestLine.Length != 3)
|
|
return 0;
|
|
|
|
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++)
|
|
{
|
|
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[name] = value;
|
|
|
|
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)
|
|
{
|
|
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
|
|
{
|
|
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 (end == form.Length)
|
|
break;
|
|
start = end + 1;
|
|
}
|
|
|
|
if (unknown != null)
|
|
PostForms["unknown"] = unknown.ToString();
|
|
}
|
|
|
|
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)
|
|
{
|
|
var delimiterStart = body.IndexOf(delimiter, position, StringComparison.Ordinal);
|
|
if (delimiterStart < 0)
|
|
return false;
|
|
|
|
position = delimiterStart + delimiter.Length;
|
|
if (position + 2 <= body.Length &&
|
|
string.CompareOrdinal(body, position, "--", 0, 2) == 0)
|
|
return true;
|
|
|
|
if (position + 2 > body.Length ||
|
|
string.CompareOrdinal(body, position, "\r\n", 0, 2) != 0)
|
|
return false;
|
|
position += 2;
|
|
|
|
var nextDelimiter = body.IndexOf("\r\n" + delimiter, position, StringComparison.Ordinal);
|
|
if (nextDelimiter < 0)
|
|
return false;
|
|
|
|
var partLength = nextDelimiter - position;
|
|
EnsureWithinLimit(partLength, MaximumMultipartPartLength, "multipart part length");
|
|
|
|
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;
|
|
}
|
|
|
|
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)
|
|
{
|
|
var parameterEnd = contentType.IndexOf(';', parameterStart + 1);
|
|
if (parameterEnd < 0)
|
|
parameterEnd = contentType.Length;
|
|
|
|
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;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void EnsureStringWithinLimit(string value, int limit, string kind)
|
|
=> EnsureStringWithinLimit(value?.Length ?? 0, limit, kind);
|
|
|
|
private static void EnsureStringWithinLimit(int length, int limit, string kind)
|
|
=> EnsureWithinLimit(length, limit, kind);
|
|
|
|
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}.");
|
|
}
|
|
}
|