using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; namespace NetCoreServer { /// /// HTTP request is used to create or process parameters of HTTP protocol request(method, URL, headers, etc). /// /// Not thread-safe. public class HttpRequest { /// /// Initialize an empty HTTP request /// public HttpRequest() { Clear(); } /// /// Initialize a new HTTP request with a given method, URL and protocol /// /// HTTP method /// Requested URL /// Protocol version (default is "HTTP/1.1") public HttpRequest(string method, string url, string protocol = "HTTP/1.1") { SetBegin(method, url, protocol); } /// /// Is the HTTP request empty? /// public bool IsEmpty { get { return (_cache.Size == 0); } } /// /// Is the HTTP request error flag set? /// public bool IsErrorSet { get; private set; } /// /// Get the HTTP request method /// public string Method { get { return _method; } } /// /// Get the HTTP request URL /// public string Url { get { return _url; } } /// /// Get the HTTP request protocol version /// public string Protocol { get { return _protocol; } } /// /// Get the HTTP request headers count /// public long Headers { get { return _headers.Count; } } /// /// Get the HTTP request header by index /// public (string, string) Header(int i) { Debug.Assert((i < _headers.Count), "Index out of bounds!"); if (i >= _headers.Count) return ("", ""); return _headers[i]; } /// /// Get the HTTP request cookies count /// public long Cookies { get { return _cookies.Count; } } /// /// Get the HTTP request cookie by index /// public (string, string) Cookie(int i) { Debug.Assert((i < _cookies.Count), "Index out of bounds!"); if (i >= _cookies.Count) return ("", ""); return _cookies[i]; } /// /// Get the HTTP request body as string /// public string Body { get { return _cache.ExtractString(_bodyIndex, _bodySize); } } /// /// Get the HTTP request body as byte array /// public byte[] BodyBytes { get { return _cache.Data[_bodyIndex..(_bodyIndex + _bodySize)]; } } /// /// Get the HTTP request body as byte span /// public Span BodySpan { get { return new Span(_cache.Data, _bodyIndex, _bodySize); } } /// /// Get the HTTP request body length /// public long BodyLength { get { return _bodyLength; } } /// /// Get the HTTP request cache content /// public Buffer Cache { get { return _cache; } } /// /// Get string from the current HTTP request /// 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(); } /// /// Clear the HTTP request cache /// 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; } /// /// Set the HTTP request begin with a given method, URL and protocol /// /// HTTP method /// Requested URL /// Protocol version (default is "HTTP/1.1") 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; } /// /// Set the HTTP request header /// /// Header key /// Header value 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; } /// /// Set the HTTP request cookie /// /// Cookie name /// Cookie value 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; } /// /// Add the HTTP request cookie /// /// Cookie name /// Cookie value 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; } /// /// Set the HTTP request body /// /// Body string content (default is "") public HttpRequest SetBody(string body = "") => SetBody(body.AsSpan()); /// /// Set the HTTP request body /// /// Body string content as a span of characters public HttpRequest SetBody(ReadOnlySpan 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; } /// /// Set the HTTP request body /// /// Body binary content public HttpRequest SetBody(byte[] body) => SetBody(body.AsSpan()); /// /// Set the HTTP request body /// /// Body binary content as a span of bytes public HttpRequest SetBody(ReadOnlySpan 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; } /// /// Set the HTTP request body length /// /// Body length 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; } /// /// Make HEAD request /// /// URL to request public HttpRequest MakeHeadRequest(string url) { Clear(); SetBegin("HEAD", url); SetBody(); return this; } /// /// Make GET request /// /// URL to request public HttpRequest MakeGetRequest(string url) { Clear(); SetBegin("GET", url); SetBody(); return this; } /// /// Make POST request /// /// URL to request /// String content /// Content type (default is "text/plain; charset=UTF-8") public HttpRequest MakePostRequest(string url, string content, string contentType = "text/plain; charset=UTF-8") => MakePostRequest(url, content.AsSpan(), contentType); /// /// Make POST request /// /// URL to request /// String content as a span of characters /// Content type (default is "text/plain; charset=UTF-8") public HttpRequest MakePostRequest(string url, ReadOnlySpan content, string contentType = "text/plain; charset=UTF-8") { Clear(); SetBegin("POST", url); if (!string.IsNullOrEmpty(contentType)) SetHeader("Content-Type", contentType); SetBody(content); return this; } /// /// Make POST request /// /// URL to request /// Binary content /// Content type (default is "") public HttpRequest MakePostRequest(string url, byte[] content, string contentType = "") => MakePostRequest(url, content.AsSpan(), contentType); /// /// Make POST request /// /// URL to request /// Binary content as a span of bytes /// Content type (default is "") public HttpRequest MakePostRequest(string url, ReadOnlySpan content, string contentType = "") { Clear(); SetBegin("POST", url); if (!string.IsNullOrEmpty(contentType)) SetHeader("Content-Type", contentType); SetBody(content); return this; } /// /// Make PUT request /// /// URL to request /// String content /// Content type (default is "text/plain; charset=UTF-8") public HttpRequest MakePutRequest(string url, string content, string contentType = "text/plain; charset=UTF-8") => MakePutRequest(url, content.AsSpan(), contentType); /// /// Make PUT request /// /// URL to request /// String content as a span of characters /// Content type (default is "text/plain; charset=UTF-8") public HttpRequest MakePutRequest(string url, ReadOnlySpan content, string contentType = "text/plain; charset=UTF-8") { Clear(); SetBegin("PUT", url); if (!string.IsNullOrEmpty(contentType)) SetHeader("Content-Type", contentType); SetBody(content); return this; } /// /// Make PUT request /// /// URL to request /// Binary content /// Content type (default is "") public HttpRequest MakePutRequest(string url, byte[] content, string contentType = "") => MakePutRequest(url, content.AsSpan(), contentType); /// /// Make PUT request /// /// URL to request /// Binary content as a span of bytes /// Content type (default is "") public HttpRequest MakePutRequest(string url, ReadOnlySpan content, string contentType = "") { Clear(); SetBegin("PUT", url); if (!string.IsNullOrEmpty(contentType)) SetHeader("Content-Type", contentType); SetBody(content); return this; } /// /// Make DELETE request /// /// URL to request public HttpRequest MakeDeleteRequest(string url) { Clear(); SetBegin("DELETE", url); SetBody(); return this; } /// /// Make OPTIONS request /// /// URL to request public HttpRequest MakeOptionsRequest(string url) { Clear(); SetBegin("OPTIONS", url); SetBody(); return this; } /// /// Make TRACE request /// /// URL to request 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; } } }