123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831 |
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Text;
- namespace NetCoreServer
- {
- /// <summary>
- /// HTTP request is used to create or process parameters of HTTP protocol request(method, URL, headers, etc).
- /// </summary>
- /// <remarks>Not thread-safe.</remarks>
- public class HttpRequest
- {
- /// <summary>
- /// Initialize an empty HTTP request
- /// </summary>
- public HttpRequest()
- {
- Clear();
- }
- /// <summary>
- /// Initialize a new HTTP request with a given method, URL and protocol
- /// </summary>
- /// <param name="method">HTTP method</param>
- /// <param name="url">Requested URL</param>
- /// <param name="protocol">Protocol version (default is "HTTP/1.1")</param>
- public HttpRequest(string method, string url, string protocol = "HTTP/1.1")
- {
- SetBegin(method, url, protocol);
- }
- /// <summary>
- /// Is the HTTP request empty?
- /// </summary>
- public bool IsEmpty { get { return (_cache.Size == 0); } }
- /// <summary>
- /// Is the HTTP request error flag set?
- /// </summary>
- public bool IsErrorSet { get; private set; }
- /// <summary>
- /// Get the HTTP request method
- /// </summary>
- public string Method { get { return _method; } }
- /// <summary>
- /// Get the HTTP request URL
- /// </summary>
- public string Url { get { return _url; } }
- /// <summary>
- /// Get the HTTP request protocol version
- /// </summary>
- public string Protocol { get { return _protocol; } }
- /// <summary>
- /// Get the HTTP request headers count
- /// </summary>
- public long Headers { get { return _headers.Count; } }
- /// <summary>
- /// Get the HTTP request header by index
- /// </summary>
- public (string, string) Header(int i)
- {
- Debug.Assert((i < _headers.Count), "Index out of bounds!");
- if (i >= _headers.Count)
- return ("", "");
- return _headers[i];
- }
- /// <summary>
- /// Get the HTTP request cookies count
- /// </summary>
- public long Cookies { get { return _cookies.Count; } }
- /// <summary>
- /// Get the HTTP request cookie by index
- /// </summary>
- public (string, string) Cookie(int i)
- {
- Debug.Assert((i < _cookies.Count), "Index out of bounds!");
- if (i >= _cookies.Count)
- return ("", "");
- return _cookies[i];
- }
- /// <summary>
- /// Get the HTTP request body as string
- /// </summary>
- public string Body { get { return _cache.ExtractString(_bodyIndex, _bodySize); } }
- /// <summary>
- /// Get the HTTP request body as byte array
- /// </summary>
- public byte[] BodyBytes { get { return _cache.Data[_bodyIndex..(_bodyIndex + _bodySize)]; } }
- /// <summary>
- /// Get the HTTP request body as byte span
- /// </summary>
- public Span<byte> BodySpan { get { return new Span<byte>(_cache.Data, _bodyIndex, _bodySize); } }
- /// <summary>
- /// Get the HTTP request body length
- /// </summary>
- public long BodyLength { get { return _bodyLength; } }
- /// <summary>
- /// Get the HTTP request cache content
- /// </summary>
- public Buffer Cache { get { return _cache; } }
- /// <summary>
- /// Get string from the current HTTP request
- /// </summary>
- public override string ToString()
- {
- StringBuilder sb = new StringBuilder();
- sb.AppendLine($"Request method: {Method}");
- sb.AppendLine($"Request URL: {Url}");
- sb.AppendLine($"Request protocol: {Protocol}");
- sb.AppendLine($"Request headers: {Headers}");
- for (int i = 0; i < Headers; i++)
- {
- var header = Header(i);
- sb.AppendLine($"{header.Item1} : {header.Item2}");
- }
- sb.AppendLine($"Request body: {BodyLength}");
- sb.AppendLine(Body);
- return sb.ToString();
- }
- /// <summary>
- /// Clear the HTTP request cache
- /// </summary>
- public HttpRequest Clear()
- {
- IsErrorSet = false;
- _method = "";
- _url = "";
- _protocol = "";
- _headers.Clear();
- _cookies.Clear();
- _bodyIndex = 0;
- _bodySize = 0;
- _bodyLength = 0;
- _bodyLengthProvided = false;
- _cache.Clear();
- _cacheSize = 0;
- return this;
- }
- /// <summary>
- /// Set the HTTP request begin with a given method, URL and protocol
- /// </summary>
- /// <param name="method">HTTP method</param>
- /// <param name="url">Requested URL</param>
- /// <param name="protocol">Protocol version (default is "HTTP/1.1")</param>
- public HttpRequest SetBegin(string method, string url, string protocol = "HTTP/1.1")
- {
- // Clear the HTTP request cache
- Clear();
- // Append the HTTP request method
- _cache.Append(method);
- _method = method;
- _cache.Append(" ");
- // Append the HTTP request URL
- _cache.Append(url);
- _url = url;
- _cache.Append(" ");
- // Append the HTTP request protocol version
- _cache.Append(protocol);
- _protocol = protocol;
- _cache.Append("\r\n");
- return this;
- }
- /// <summary>
- /// Set the HTTP request header
- /// </summary>
- /// <param name="key">Header key</param>
- /// <param name="value">Header value</param>
- public HttpRequest SetHeader(string key, string value)
- {
- // Append the HTTP request header's key
- _cache.Append(key);
- _cache.Append(": ");
- // Append the HTTP request header's value
- _cache.Append(value);
- _cache.Append("\r\n");
- // Add the header to the corresponding collection
- _headers.Add((key, value));
- return this;
- }
- /// <summary>
- /// Set the HTTP request cookie
- /// </summary>
- /// <param name="name">Cookie name</param>
- /// <param name="value">Cookie value</param>
- public HttpRequest SetCookie(string name, string value)
- {
- string key = "Cookie";
- string cookie = name + "=" + value;
- // Append the HTTP request header's key
- _cache.Append(key);
- _cache.Append(": ");
- // Append Cookie
- _cache.Append(cookie);
- _cache.Append("\r\n");
- // Add the header to the corresponding collection
- _headers.Add((key, cookie));
- // Add the cookie to the corresponding collection
- _cookies.Add((name, value));
- return this;
- }
- /// <summary>
- /// Add the HTTP request cookie
- /// </summary>
- /// <param name="name">Cookie name</param>
- /// <param name="value">Cookie value</param>
- public HttpRequest AddCookie(string name, string value)
- {
- // Append Cookie
- _cache.Append("; ");
- _cache.Append(name);
- _cache.Append("=");
- _cache.Append(value);
- // Add the cookie to the corresponding collection
- _cookies.Add((name, value));
- return this;
- }
- /// <summary>
- /// Set the HTTP request body
- /// </summary>
- /// <param name="body">Body string content (default is "")</param>
- public HttpRequest SetBody(string body = "") => SetBody(body.AsSpan());
- /// <summary>
- /// Set the HTTP request body
- /// </summary>
- /// <param name="body">Body string content as a span of characters</param>
- public HttpRequest SetBody(ReadOnlySpan<char> body)
- {
- int length = body.IsEmpty ? 0 : Encoding.UTF8.GetByteCount(body);
- // Append content length header
- SetHeader("Content-Length", length.ToString());
- _cache.Append("\r\n");
- int index = (int)_cache.Size;
- // Append the HTTP request body
- _cache.Append(body);
- _bodyIndex = index;
- _bodySize = length;
- _bodyLength = length;
- _bodyLengthProvided = true;
- return this;
- }
- /// <summary>
- /// Set the HTTP request body
- /// </summary>
- /// <param name="body">Body binary content</param>
- public HttpRequest SetBody(byte[] body) => SetBody(body.AsSpan());
- /// <summary>
- /// Set the HTTP request body
- /// </summary>
- /// <param name="body">Body binary content as a span of bytes</param>
- public HttpRequest SetBody(ReadOnlySpan<byte> body)
- {
- // Append content length header
- SetHeader("Content-Length", body.Length.ToString());
- _cache.Append("\r\n");
- int index = (int)_cache.Size;
- // Append the HTTP request body
- _cache.Append(body);
- _bodyIndex = index;
- _bodySize = body.Length;
- _bodyLength = body.Length;
- _bodyLengthProvided = true;
- return this;
- }
- /// <summary>
- /// Set the HTTP request body length
- /// </summary>
- /// <param name="length">Body length</param>
- public HttpRequest SetBodyLength(int length)
- {
- // Append content length header
- SetHeader("Content-Length", length.ToString());
- _cache.Append("\r\n");
- int index = (int)_cache.Size;
- // Clear the HTTP request body
- _bodyIndex = index;
- _bodySize = 0;
- _bodyLength = length;
- _bodyLengthProvided = true;
- return this;
- }
- /// <summary>
- /// Make HEAD request
- /// </summary>
- /// <param name="url">URL to request</param>
- public HttpRequest MakeHeadRequest(string url)
- {
- Clear();
- SetBegin("HEAD", url);
- SetBody();
- return this;
- }
- /// <summary>
- /// Make GET request
- /// </summary>
- /// <param name="url">URL to request</param>
- public HttpRequest MakeGetRequest(string url)
- {
- Clear();
- SetBegin("GET", url);
- SetBody();
- return this;
- }
- /// <summary>
- /// Make POST request
- /// </summary>
- /// <param name="url">URL to request</param>
- /// <param name="content">String content</param>
- /// <param name="contentType">Content type (default is "text/plain; charset=UTF-8")</param>
- public HttpRequest MakePostRequest(string url, string content, string contentType = "text/plain; charset=UTF-8") => MakePostRequest(url, content.AsSpan(), contentType);
- /// <summary>
- /// Make POST request
- /// </summary>
- /// <param name="url">URL to request</param>
- /// <param name="content">String content as a span of characters</param>
- /// <param name="contentType">Content type (default is "text/plain; charset=UTF-8")</param>
- public HttpRequest MakePostRequest(string url, ReadOnlySpan<char> content, string contentType = "text/plain; charset=UTF-8")
- {
- Clear();
- SetBegin("POST", url);
- if (!string.IsNullOrEmpty(contentType))
- SetHeader("Content-Type", contentType);
- SetBody(content);
- return this;
- }
- /// <summary>
- /// Make POST request
- /// </summary>
- /// <param name="url">URL to request</param>
- /// <param name="content">Binary content</param>
- /// <param name="contentType">Content type (default is "")</param>
- public HttpRequest MakePostRequest(string url, byte[] content, string contentType = "") => MakePostRequest(url, content.AsSpan(), contentType);
- /// <summary>
- /// Make POST request
- /// </summary>
- /// <param name="url">URL to request</param>
- /// <param name="content">Binary content as a span of bytes</param>
- /// <param name="contentType">Content type (default is "")</param>
- public HttpRequest MakePostRequest(string url, ReadOnlySpan<byte> content, string contentType = "")
- {
- Clear();
- SetBegin("POST", url);
- if (!string.IsNullOrEmpty(contentType))
- SetHeader("Content-Type", contentType);
- SetBody(content);
- return this;
- }
- /// <summary>
- /// Make PUT request
- /// </summary>
- /// <param name="url">URL to request</param>
- /// <param name="content">String content</param>
- /// <param name="contentType">Content type (default is "text/plain; charset=UTF-8")</param>
- public HttpRequest MakePutRequest(string url, string content, string contentType = "text/plain; charset=UTF-8") => MakePutRequest(url, content.AsSpan(), contentType);
- /// <summary>
- /// Make PUT request
- /// </summary>
- /// <param name="url">URL to request</param>
- /// <param name="content">String content as a span of characters</param>
- /// <param name="contentType">Content type (default is "text/plain; charset=UTF-8")</param>
- public HttpRequest MakePutRequest(string url, ReadOnlySpan<char> content, string contentType = "text/plain; charset=UTF-8")
- {
- Clear();
- SetBegin("PUT", url);
- if (!string.IsNullOrEmpty(contentType))
- SetHeader("Content-Type", contentType);
- SetBody(content);
- return this;
- }
- /// <summary>
- /// Make PUT request
- /// </summary>
- /// <param name="url">URL to request</param>
- /// <param name="content">Binary content</param>
- /// <param name="contentType">Content type (default is "")</param>
- public HttpRequest MakePutRequest(string url, byte[] content, string contentType = "") => MakePutRequest(url, content.AsSpan(), contentType);
- /// <summary>
- /// Make PUT request
- /// </summary>
- /// <param name="url">URL to request</param>
- /// <param name="content">Binary content as a span of bytes</param>
- /// <param name="contentType">Content type (default is "")</param>
- public HttpRequest MakePutRequest(string url, ReadOnlySpan<byte> content, string contentType = "")
- {
- Clear();
- SetBegin("PUT", url);
- if (!string.IsNullOrEmpty(contentType))
- SetHeader("Content-Type", contentType);
- SetBody(content);
- return this;
- }
- /// <summary>
- /// Make DELETE request
- /// </summary>
- /// <param name="url">URL to request</param>
- public HttpRequest MakeDeleteRequest(string url)
- {
- Clear();
- SetBegin("DELETE", url);
- SetBody();
- return this;
- }
- /// <summary>
- /// Make OPTIONS request
- /// </summary>
- /// <param name="url">URL to request</param>
- public HttpRequest MakeOptionsRequest(string url)
- {
- Clear();
- SetBegin("OPTIONS", url);
- SetBody();
- return this;
- }
- /// <summary>
- /// Make TRACE request
- /// </summary>
- /// <param name="url">URL to request</param>
- public HttpRequest MakeTraceRequest(string url)
- {
- Clear();
- SetBegin("TRACE", url);
- SetBody();
- return this;
- }
- // HTTP request method
- private string _method;
- // HTTP request URL
- private string _url;
- // HTTP request protocol
- private string _protocol;
- // HTTP request headers
- private List<(string, string)> _headers = new List<(string, string)>();
- // HTTP request cookies
- private List<(string, string)> _cookies = new List<(string, string)>();
- // HTTP request body
- private int _bodyIndex;
- private int _bodySize;
- private int _bodyLength;
- private bool _bodyLengthProvided;
- // HTTP request cache
- private Buffer _cache = new Buffer();
- private int _cacheSize;
- // Is pending parts of HTTP request
- internal bool IsPendingHeader()
- {
- return (!IsErrorSet && (_bodyIndex == 0));
- }
- internal bool IsPendingBody()
- {
- return (!IsErrorSet && (_bodyIndex > 0) && (_bodySize > 0));
- }
- internal bool ReceiveHeader(byte[] buffer, int offset, int size)
- {
- // Update the request cache
- _cache.Append(buffer, offset, size);
- // Try to seek for HTTP header separator
- for (int i = _cacheSize; i < (int)_cache.Size; i++)
- {
- // Check for the request cache out of bounds
- if ((i + 3) >= (int)_cache.Size)
- break;
- // Check for the header separator
- if ((_cache[i + 0] == '\r') && (_cache[i + 1] == '\n') && (_cache[i + 2] == '\r') && (_cache[i + 3] == '\n'))
- {
- int index = 0;
- // Set the error flag for a while...
- IsErrorSet = true;
- // Parse method
- int methodIndex = index;
- int methodSize = 0;
- while (_cache[index] != ' ')
- {
- methodSize++;
- index++;
- if (index >= (int)_cache.Size)
- return false;
- }
- index++;
- if (index >= (int)_cache.Size)
- return false;
- _method = _cache.ExtractString(methodIndex, methodSize);
- // Parse URL
- int urlIndex = index;
- int urlSize = 0;
- while (_cache[index] != ' ')
- {
- urlSize++;
- index++;
- if (index >= (int)_cache.Size)
- return false;
- }
- index++;
- if (index >= (int)_cache.Size)
- return false;
- _url = _cache.ExtractString(urlIndex, urlSize);
- // Parse protocol version
- int protocolIndex = index;
- int protocolSize = 0;
- while (_cache[index] != '\r')
- {
- protocolSize++;
- index++;
- if (index >= (int)_cache.Size)
- return false;
- }
- index++;
- if ((index >= (int)_cache.Size) || (_cache[index] != '\n'))
- return false;
- index++;
- if (index >= (int)_cache.Size)
- return false;
- _protocol = _cache.ExtractString(protocolIndex, protocolSize);
- // Parse headers
- while ((index < (int)_cache.Size) && (index < i))
- {
- // Parse header name
- int headerNameIndex = index;
- int headerNameSize = 0;
- while (_cache[index] != ':')
- {
- headerNameSize++;
- index++;
- if (index >= i)
- break;
- if (index >= (int)_cache.Size)
- return false;
- }
- index++;
- if (index >= i)
- break;
- if (index >= (int)_cache.Size)
- return false;
- // Skip all prefix space characters
- while (char.IsWhiteSpace((char)_cache[index]))
- {
- index++;
- if (index >= i)
- break;
- if (index >= (int)_cache.Size)
- return false;
- }
- // Parse header value
- int headerValueIndex = index;
- int headerValueSize = 0;
- while (_cache[index] != '\r')
- {
- headerValueSize++;
- index++;
- if (index >= i)
- break;
- if (index >= (int)_cache.Size)
- return false;
- }
- index++;
- if ((index >= (int)_cache.Size) || (_cache[index] != '\n'))
- return false;
- index++;
- if (index >= (int)_cache.Size)
- return false;
- // Validate header name and value (sometimes value can be empty)
- if (headerNameSize == 0)
- return false;
- // Add a new header
- string headerName = _cache.ExtractString(headerNameIndex, headerNameSize);
- string headerValue = _cache.ExtractString(headerValueIndex, headerValueSize);
- _headers.Add((headerName, headerValue));
- // Try to find the body content length
- if (string.Compare(headerName, "Content-Length", StringComparison.OrdinalIgnoreCase) == 0)
- {
- _bodyLength = 0;
- for (int j = headerValueIndex; j < (headerValueIndex + headerValueSize); j++)
- {
- if ((_cache[j] < '0') || (_cache[j] > '9'))
- return false;
- _bodyLength *= 10;
- _bodyLength += _cache[j] - '0';
- _bodyLengthProvided = true;
- }
- }
- // Try to find Cookies
- if (string.Compare(headerName, "Cookie", StringComparison.OrdinalIgnoreCase) == 0)
- {
- bool name = true;
- bool token = false;
- int current = headerValueIndex;
- int nameIndex = index;
- int nameSize = 0;
- int cookieIndex = index;
- int cookieSize = 0;
- for (int j = headerValueIndex; j < (headerValueIndex + headerValueSize); j++)
- {
- if (_cache[j] == ' ')
- {
- if (token)
- {
- if (name)
- {
- nameIndex = current;
- nameSize = j - current;
- }
- else
- {
- cookieIndex = current;
- cookieSize = j - current;
- }
- }
- token = false;
- continue;
- }
- if (_cache[j] == '=')
- {
- if (token)
- {
- if (name)
- {
- nameIndex = current;
- nameSize = j - current;
- }
- else
- {
- cookieIndex = current;
- cookieSize = j - current;
- }
- }
- token = false;
- name = false;
- continue;
- }
- if (_cache[j] == ';')
- {
- if (token)
- {
- if (name)
- {
- nameIndex = current;
- nameSize = j - current;
- }
- else
- {
- cookieIndex = current;
- cookieSize = j - current;
- }
- // Validate the cookie
- if ((nameSize > 0) && (cookieSize > 0))
- {
- // Add the cookie to the corresponding collection
- _cookies.Add((_cache.ExtractString(nameIndex, nameSize), _cache.ExtractString(cookieIndex, cookieSize)));
- // Resset the current cookie values
- nameIndex = j;
- nameSize = 0;
- cookieIndex = j;
- cookieSize = 0;
- }
- }
- token = false;
- name = true;
- continue;
- }
- if (!token)
- {
- current = j;
- token = true;
- }
- }
- // Process the last cookie
- if (token)
- {
- if (name)
- {
- nameIndex = current;
- nameSize = headerValueIndex + headerValueSize - current;
- }
- else
- {
- cookieIndex = current;
- cookieSize = headerValueIndex + headerValueSize - current;
- }
- // Validate the cookie
- if ((nameSize > 0) && (cookieSize > 0))
- {
- // Add the cookie to the corresponding collection
- _cookies.Add((_cache.ExtractString(nameIndex, nameSize), _cache.ExtractString(cookieIndex, cookieSize)));
- }
- }
- }
- }
- // Reset the error flag
- IsErrorSet = false;
- // Update the body index and size
- _bodyIndex = i + 4;
- _bodySize = (int)_cache.Size - i - 4;
- // Update the parsed cache size
- _cacheSize = (int)_cache.Size;
- return true;
- }
- }
- // Update the parsed cache size
- _cacheSize = ((int)_cache.Size >= 3) ? ((int)_cache.Size - 3) : 0;
- return false;
- }
- internal bool ReceiveBody(byte[] buffer, int offset, int size)
- {
- // Update the request cache
- _cache.Append(buffer, offset, size);
- // Update the parsed cache size
- _cacheSize = (int)_cache.Size;
- // Update body size
- _bodySize += size;
- // Check if the body length was provided
- if (_bodyLengthProvided)
- {
- // Was the body fully received?
- if (_bodySize >= _bodyLength)
- {
- _bodySize = _bodyLength;
- return true;
- }
- }
- else
- {
- // HEAD/GET/DELETE/OPTIONS/TRACE request might have no body
- if ((Method == "HEAD") || (Method == "GET") || (Method == "DELETE") || (Method == "OPTIONS") || (Method == "TRACE"))
- {
- _bodyLength = 0;
- _bodySize = 0;
- return true;
- }
- // Check the body content to find the request body end
- if (_bodySize >= 4)
- {
- int index = _bodyIndex + _bodySize - 4;
- // Was the body fully received?
- if ((_cache[index + 0] == '\r') && (_cache[index + 1] == '\n') && (_cache[index + 2] == '\r') && (_cache[index + 3] == '\n'))
- {
- _bodyLength = _bodySize;
- return true;
- }
- }
- }
- // Body was received partially...
- return false;
- }
- }
- }
|