using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; namespace NetCoreServer { /// <summary> /// HTTP response is used to create or process parameters of HTTP protocol response(status, headers, etc). /// </summary> /// <remarks>Not thread-safe.</remarks> public class HttpResponse { static HttpResponse() { _mimeTable = new Dictionary<string, string> { // Base content types { ".html", "text/html" }, { ".css", "text/css" }, { ".js", "text/javascript" }, { ".vue", "text/html" }, { ".xml", "text/xml" }, // Application content types { ".atom", "application/atom+xml" }, { ".fastsoap", "application/fastsoap" }, { ".gzip", "application/gzip" }, { ".json", "application/json" }, { ".map", "application/json" }, { ".pdf", "application/pdf" }, { ".ps", "application/postscript" }, { ".soap", "application/soap+xml" }, { ".sql", "application/sql" }, { ".xslt", "application/xslt+xml" }, { ".zip", "application/zip" }, { ".zlib", "application/zlib" }, // Audio content types { ".aac", "audio/aac" }, { ".ac3", "audio/ac3" }, { ".mp3", "audio/mpeg" }, { ".ogg", "audio/ogg" }, // Font content types { ".ttf", "font/ttf" }, // Image content types { ".bmp", "image/bmp" }, { ".emf", "image/emf" }, { ".gif", "image/gif" }, { ".jpg", "image/jpeg" }, { ".jpm", "image/jpm" }, { ".jpx", "image/jpx" }, { ".jrx", "image/jrx" }, { ".png", "image/png" }, { ".svg", "image/svg+xml" }, { ".tiff", "image/tiff" }, { ".wmf", "image/wmf" }, // Message content types { ".http", "message/http" }, { ".s-http", "message/s-http" }, // Model content types { ".mesh", "model/mesh" }, { ".vrml", "model/vrml" }, // Text content types { ".csv", "text/csv" }, { ".plain", "text/plain" }, { ".richtext", "text/richtext" }, { ".rtf", "text/rtf" }, { ".rtx", "text/rtx" }, { ".sgml", "text/sgml" }, { ".strings", "text/strings" }, { ".url", "text/uri-list" }, // Video content types { ".H264", "video/H264" }, { ".H265", "video/H265" }, { ".mp4", "video/mp4" }, { ".mpeg", "video/mpeg" }, { ".raw", "video/raw" } }; } /// <summary> /// Initialize an empty HTTP response /// </summary> public HttpResponse() { Clear(); } /// <summary> /// Initialize a new HTTP response with a given status and protocol /// </summary> /// <param name="status">HTTP status</param> /// <param name="protocol">Protocol version (default is "HTTP/1.1")</param> public HttpResponse(int status, string protocol = "HTTP/1.1") { SetBegin(status, protocol); } /// <summary> /// Initialize a new HTTP response with a given status, status phrase and protocol /// </summary> /// <param name="status">HTTP status</param> /// <param name="statusPhrase">HTTP status phrase</param> /// <param name="protocol">Protocol version</param> public HttpResponse(int status, string statusPhrase, string protocol) { SetBegin(status, statusPhrase, protocol); } /// <summary> /// Is the HTTP response empty? /// </summary> public bool IsEmpty { get { return (_cache.Size > 0); } } /// <summary> /// Is the HTTP response error flag set? /// </summary> public bool IsErrorSet { get; private set; } /// <summary> /// Get the HTTP response status /// </summary> public int Status { get; private set; } /// <summary> /// Get the HTTP response status phrase /// </summary> public string StatusPhrase { get { return _statusPhrase; } } /// <summary> /// Get the HTTP response protocol version /// </summary> public string Protocol { get { return _protocol; } } /// <summary> /// Get the HTTP response headers count /// </summary> public long Headers { get { return _headers.Count; } } /// <summary> /// Get the HTTP response 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 response 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 read-only byte span /// </summary> public ReadOnlySpan<byte> BodySpan { get { return new ReadOnlySpan<byte>(_cache.Data, _bodyIndex, _bodySize); } } /// <summary> /// Get the HTTP response body length /// </summary> public long BodyLength { get { return _bodyLength; } } /// <summary> /// Get the HTTP response cache content /// </summary> public Buffer Cache { get { return _cache; } } /// <summary> /// Get string from the current HTTP response /// </summary> public override string ToString() { StringBuilder sb = new StringBuilder(); sb.AppendLine($"Status: {Status}"); sb.AppendLine($"Status phrase: {StatusPhrase}"); sb.AppendLine($"Protocol: {Protocol}"); sb.AppendLine($"Headers: {Headers}"); for (int i = 0; i < Headers; i++) { var header = Header(i); sb.AppendLine($"{header.Item1} : {header.Item2}"); } sb.AppendLine($"Body: {BodyLength}"); sb.AppendLine(Body); return sb.ToString(); } /// <summary> /// Clear the HTTP response cache /// </summary> public HttpResponse Clear() { IsErrorSet = false; Status = 0; _statusPhrase = ""; _protocol = ""; _headers.Clear(); _bodyIndex = 0; _bodySize = 0; _bodyLength = 0; _bodyLengthProvided = false; _cache.Clear(); _cacheSize = 0; return this; } /// <summary> /// Set the HTTP response begin with a given status and protocol /// </summary> /// <param name="status">HTTP status</param> /// <param name="protocol">Protocol version (default is "HTTP/1.1")</param> public HttpResponse SetBegin(int status, string protocol = "HTTP/1.1") { string statusPhrase; switch (status) { case 100: statusPhrase = "Continue"; break; case 101: statusPhrase = "Switching Protocols"; break; case 102: statusPhrase = "Processing"; break; case 103: statusPhrase = "Early Hints"; break; case 200: statusPhrase = "OK"; break; case 201: statusPhrase = "Created"; break; case 202: statusPhrase = "Accepted"; break; case 203: statusPhrase = "Non-Authoritative Information"; break; case 204: statusPhrase = "No Content"; break; case 205: statusPhrase = "Reset Content"; break; case 206: statusPhrase = "Partial Content"; break; case 207: statusPhrase = "Multi-Status"; break; case 208: statusPhrase = "Already Reported"; break; case 226: statusPhrase = "IM Used"; break; case 300: statusPhrase = "Multiple Choices"; break; case 301: statusPhrase = "Moved Permanently"; break; case 302: statusPhrase = "Found"; break; case 303: statusPhrase = "See Other"; break; case 304: statusPhrase = "Not Modified"; break; case 305: statusPhrase = "Use Proxy"; break; case 306: statusPhrase = "Switch Proxy"; break; case 307: statusPhrase = "Temporary Redirect"; break; case 308: statusPhrase = "Permanent Redirect"; break; case 400: statusPhrase = "Bad Request"; break; case 401: statusPhrase = "Unauthorized"; break; case 402: statusPhrase = "Payment Required"; break; case 403: statusPhrase = "Forbidden"; break; case 404: statusPhrase = "Not Found"; break; case 405: statusPhrase = "Method Not Allowed"; break; case 406: statusPhrase = "Not Acceptable"; break; case 407: statusPhrase = "Proxy Authentication Required"; break; case 408: statusPhrase = "Request Timeout"; break; case 409: statusPhrase = "Conflict"; break; case 410: statusPhrase = "Gone"; break; case 411: statusPhrase = "Length Required"; break; case 412: statusPhrase = "Precondition Failed"; break; case 413: statusPhrase = "Payload Too Large"; break; case 414: statusPhrase = "URI Too Long"; break; case 415: statusPhrase = "Unsupported Media Type"; break; case 416: statusPhrase = "Range Not Satisfiable"; break; case 417: statusPhrase = "Expectation Failed"; break; case 421: statusPhrase = "Misdirected Request"; break; case 422: statusPhrase = "Unprocessable Entity"; break; case 423: statusPhrase = "Locked"; break; case 424: statusPhrase = "Failed Dependency"; break; case 425: statusPhrase = "Too Early"; break; case 426: statusPhrase = "Upgrade Required"; break; case 427: statusPhrase = "Unassigned"; break; case 428: statusPhrase = "Precondition Required"; break; case 429: statusPhrase = "Too Many Requests"; break; case 431: statusPhrase = "Request Header Fields Too Large"; break; case 451: statusPhrase = "Unavailable For Legal Reasons"; break; case 500: statusPhrase = "Internal Server Error"; break; case 501: statusPhrase = "Not Implemented"; break; case 502: statusPhrase = "Bad Gateway"; break; case 503: statusPhrase = "Service Unavailable"; break; case 504: statusPhrase = "Gateway Timeout"; break; case 505: statusPhrase = "HTTP Version Not Supported"; break; case 506: statusPhrase = "Variant Also Negotiates"; break; case 507: statusPhrase = "Insufficient Storage"; break; case 508: statusPhrase = "Loop Detected"; break; case 510: statusPhrase = "Not Extended"; break; case 511: statusPhrase = "Network Authentication Required"; break; default: statusPhrase = "Unknown"; break; } SetBegin(status, statusPhrase, protocol); return this; } /// <summary> /// Set the HTTP response begin with a given status, status phrase and protocol /// </summary> /// <param name="status">HTTP status</param> /// <param name="statusPhrase"> HTTP status phrase</param> /// <param name="protocol">Protocol version</param> public HttpResponse SetBegin(int status, string statusPhrase, string protocol) { // Clear the HTTP response cache Clear(); // Append the HTTP response protocol version _cache.Append(protocol); _protocol = protocol; _cache.Append(" "); // Append the HTTP response status _cache.Append(status.ToString()); Status = status; _cache.Append(" "); // Append the HTTP response status phrase _cache.Append(statusPhrase); _statusPhrase = statusPhrase; _cache.Append("\r\n"); return this; } /// <summary> /// Set the HTTP response content type /// </summary> /// <param name="extension">Content extension</param> public HttpResponse SetContentType(string extension) { // Try to lookup the content type in mime table if (_mimeTable.TryGetValue(extension, out string mime)) return SetHeader("Content-Type", mime); return this; } /// <summary> /// Set the HTTP response header /// </summary> /// <param name="key">Header key</param> /// <param name="value">Header value</param> public HttpResponse SetHeader(string key, string value) { // Append the HTTP response header's key _cache.Append(key); _cache.Append(": "); // Append the HTTP response 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 response cookie /// </summary> /// <param name="name">Cookie name</param> /// <param name="value">Cookie value</param> /// <param name="maxAge">Cookie age in seconds until it expires (default is 86400)</param> /// <param name="path">Cookie path (default is "")</param> /// <param name="domain">Cookie domain (default is "")</param> /// <param name="secure">Cookie secure flag (default is true)</param> /// <param name="strict">Cookie strict flag (default is true)</param> /// <param name="httpOnly">Cookie HTTP-only flag (default is true)</param> public HttpResponse SetCookie(string name, string value, int maxAge = 86400, string path = "", string domain = "", bool secure = true, bool strict = true, bool httpOnly = true) { string key = "Set-Cookie"; // Append the HTTP response header's key _cache.Append(key); _cache.Append(": "); // Append the HTTP response header's value int valueIndex = (int)_cache.Size; // Append cookie _cache.Append(name); _cache.Append("="); _cache.Append(value); _cache.Append("; Max-Age="); _cache.Append(maxAge.ToString()); if (!string.IsNullOrEmpty(domain)) { _cache.Append("; Domain="); _cache.Append(domain); } if (!string.IsNullOrEmpty(path)) { _cache.Append("; Path="); _cache.Append(path); } if (secure) _cache.Append("; Secure"); if (strict) _cache.Append("; SameSite=Strict"); if (httpOnly) _cache.Append("; HttpOnly"); int valueSize = (int)_cache.Size - valueIndex; string cookie = _cache.ExtractString(valueIndex, valueSize); _cache.Append("\r\n"); // Add the header to the corresponding collection _headers.Add((key, cookie)); return this; } /// <summary> /// Set the HTTP response body /// </summary> /// <param name="body">Body string content (default is "")</param> public HttpResponse SetBody(string body = "") => SetBody(body.AsSpan()); /// <summary> /// Set the HTTP response body /// </summary> /// <param name="body">Body string content as a span of characters</param> public HttpResponse 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 response body _cache.Append(body); _bodyIndex = index; _bodySize = length; _bodyLength = length; _bodyLengthProvided = true; return this; } /// <summary> /// Set the HTTP response body /// </summary> /// <param name="body">Body binary content</param> public HttpResponse SetBody(byte[] body) => SetBody(body.AsSpan()); /// <summary> /// Set the HTTP response body /// </summary> /// <param name="body">Body binary content as a span of bytes</param> public HttpResponse 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 response body _cache.Append(body); _bodyIndex = index; _bodySize = body.Length; _bodyLength = body.Length; _bodyLengthProvided = true; return this; } /// <summary> /// Set the HTTP response body length /// </summary> /// <param name="length">Body length</param> public HttpResponse SetBodyLength(int length) { // Append content length header SetHeader("Content-Length", length.ToString()); _cache.Append("\r\n"); int index = (int)_cache.Size; // Clear the HTTP response body _bodyIndex = index; _bodySize = 0; _bodyLength = length; _bodyLengthProvided = true; return this; } /// <summary> /// Make OK response /// </summary> /// <param name="status">OK status (default is 200 (OK))</param> public HttpResponse MakeOkResponse(int status = 200) { Clear(); SetBegin(status); SetBody(); return this; } /// <summary> /// Make ERROR response /// </summary> /// <param name="content">Error content (default is "")</param> /// <param name="contentType">Error content type (default is "text/plain; charset=UTF-8")</param> public HttpResponse MakeErrorResponse(string content = "", string contentType = "text/plain; charset=UTF-8") { return MakeErrorResponse(500, content, contentType); } /// <summary> /// Make ERROR response /// </summary> /// <param name="status">Error status</param> /// <param name="content">Error content (default is "")</param> /// <param name="contentType">Error content type (default is "text/plain; charset=UTF-8")</param> public HttpResponse MakeErrorResponse(int status, string content = "", string contentType = "text/plain; charset=UTF-8") { Clear(); SetBegin(status); if (!string.IsNullOrEmpty(contentType)) SetHeader("Content-Type", contentType); SetBody(content); return this; } /// <summary> /// Make HEAD response /// </summary> public HttpResponse MakeHeadResponse() { Clear(); SetBegin(200); SetBody(); return this; } /// <summary> /// Make GET response /// </summary> /// <param name="content">String content (default is "")</param> /// <param name="contentType">Content type (default is "text/plain; charset=UTF-8")</param> public HttpResponse MakeGetResponse(string content = "", string contentType = "text/plain; charset=UTF-8") => MakeGetResponse(content.AsSpan(), contentType); /// <summary> /// Make GET response /// </summary> /// <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 HttpResponse MakeGetResponse(ReadOnlySpan<char> content, string contentType = "text/plain; charset=UTF-8") { Clear(); SetBegin(200); if (!string.IsNullOrEmpty(contentType)) SetHeader("Content-Type", contentType); SetBody(content); return this; } /// <summary> /// Make GET response /// </summary> /// <param name="content">Binary content</param> /// <param name="contentType">Content type (default is "")</param> public HttpResponse MakeGetResponse(byte[] content, string contentType = "") => MakeGetResponse(content.AsSpan(), contentType); /// <summary> /// Make GET response /// </summary> /// <param name="content">Binary content as a span of bytes</param> /// <param name="contentType">Content type (default is "")</param> public HttpResponse MakeGetResponse(ReadOnlySpan<byte> content, string contentType = "") { Clear(); SetBegin(200); if (!string.IsNullOrEmpty(contentType)) SetHeader("Content-Type", contentType); SetBody(content); return this; } /// <summary> /// Make OPTIONS response /// </summary> /// <param name="allow">Allow methods (default is "HEAD,GET,POST,PUT,DELETE,OPTIONS,TRACE")</param> public HttpResponse MakeOptionsResponse(string allow = "HEAD,GET,POST,PUT,DELETE,OPTIONS,TRACE") { Clear(); SetBegin(200); SetHeader("Allow", allow); SetBody(); return this; } /// <summary> /// Make TRACE response /// </summary> /// <param name="content">String content</param> public HttpResponse MakeTraceResponse(string content) => MakeTraceResponse(content.AsSpan()); /// <summary> /// Make TRACE response /// </summary> /// <param name="content">String content as a span of characters</param> public HttpResponse MakeTraceResponse(ReadOnlySpan<char> content) { Clear(); SetBegin(200); SetHeader("Content-Type", "message/http"); SetBody(content); return this; } /// <summary> /// Make TRACE response /// </summary> /// <param name="content">Binary content</param> public HttpResponse MakeTraceResponse(byte[] content) => MakeTraceResponse(content.AsSpan()); /// <summary> /// Make TRACE response /// </summary> /// <param name="content">Binary content as a span of bytes</param> public HttpResponse MakeTraceResponse(ReadOnlySpan<byte> content) { Clear(); SetBegin(200); SetHeader("Content-Type", "message/http"); SetBody(content); return this; } /// <summary> /// Make TRACE response /// </summary> /// <param name="request">HTTP request</param> public HttpResponse MakeTraceResponse(HttpRequest request) => MakeTraceResponse(request.Cache.AsSpan()); // HTTP response status phrase private string _statusPhrase; // HTTP response protocol private string _protocol; // HTTP response headers private List<(string, string)> _headers = new List<(string, string)>(); // HTTP response body private int _bodyIndex; private int _bodySize; private int _bodyLength; private bool _bodyLengthProvided; // HTTP response cache private Buffer _cache = new Buffer(); private int _cacheSize; // HTTP response mime table private static readonly Dictionary<string, string> _mimeTable; // Is pending parts of HTTP response internal bool IsPendingHeader() { return (!IsErrorSet && (_bodyIndex == 0)); } internal bool IsPendingBody() { return (!IsErrorSet && (_bodyIndex > 0) && (_bodySize > 0)); } // Receive parts of HTTP response 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 protocol version int protocolIndex = index; int protocolSize = 0; while (_cache[index] != ' ') { protocolSize++; index++; if (index >= (int)_cache.Size) return false; } index++; if ((index >= (int)_cache.Size)) return false; _protocol = _cache.ExtractString(protocolIndex, protocolSize); // Parse status code int statusIndex = index; int statusSize = 0; while (_cache[index] != ' ') { if ((_cache[index] < '0') || (_cache[index] > '9')) return false; statusSize++; index++; if (index >= (int)_cache.Size) return false; } Status = 0; for (int j = statusIndex; j < (statusIndex + statusSize); j++) { Status *= 10; Status += _cache[j] - '0'; } index++; if (index >= (int)_cache.Size) return false; // Parse status phrase int statusPhraseIndex = index; int statusPhraseSize = 0; while (_cache[index] != '\r') { statusPhraseSize++; 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; _statusPhrase = _cache.ExtractString(statusPhraseIndex, statusPhraseSize); // 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; } } } // 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 { // Check the body content to find the response 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; } } }