using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; namespace NetCoreServer { /// /// HTTP response is used to create or process parameters of HTTP protocol response(status, headers, etc). /// /// Not thread-safe. public class HttpResponse { static HttpResponse() { _mimeTable = new Dictionary { // 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" } }; } /// /// Initialize an empty HTTP response /// public HttpResponse() { Clear(); } /// /// Initialize a new HTTP response with a given status and protocol /// /// HTTP status /// Protocol version (default is "HTTP/1.1") public HttpResponse(int status, string protocol = "HTTP/1.1") { SetBegin(status, protocol); } /// /// Initialize a new HTTP response with a given status, status phrase and protocol /// /// HTTP status /// HTTP status phrase /// Protocol version public HttpResponse(int status, string statusPhrase, string protocol) { SetBegin(status, statusPhrase, protocol); } /// /// Is the HTTP response empty? /// public bool IsEmpty { get { return (_cache.Size > 0); } } /// /// Is the HTTP response error flag set? /// public bool IsErrorSet { get; private set; } /// /// Get the HTTP response status /// public int Status { get; private set; } /// /// Get the HTTP response status phrase /// public string StatusPhrase { get { return _statusPhrase; } } /// /// Get the HTTP response protocol version /// public string Protocol { get { return _protocol; } } /// /// Get the HTTP response headers count /// public long Headers { get { return _headers.Count; } } /// /// Get the HTTP response 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 response 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 read-only byte span /// public ReadOnlySpan BodySpan { get { return new ReadOnlySpan(_cache.Data, _bodyIndex, _bodySize); } } /// /// Get the HTTP response body length /// public long BodyLength { get { return _bodyLength; } } /// /// Get the HTTP response cache content /// public Buffer Cache { get { return _cache; } } /// /// Get string from the current HTTP response /// 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(); } /// /// Clear the HTTP response cache /// 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; } /// /// Set the HTTP response begin with a given status and protocol /// /// HTTP status /// Protocol version (default is "HTTP/1.1") 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; } /// /// Set the HTTP response begin with a given status, status phrase and protocol /// /// HTTP status /// HTTP status phrase /// Protocol version 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; } /// /// Set the HTTP response content type /// /// Content extension 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; } /// /// Set the HTTP response header /// /// Header key /// Header value 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; } /// /// Set the HTTP response cookie /// /// Cookie name /// Cookie value /// Cookie age in seconds until it expires (default is 86400) /// Cookie path (default is "") /// Cookie domain (default is "") /// Cookie secure flag (default is true) /// Cookie strict flag (default is true) /// Cookie HTTP-only flag (default is true) 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; } /// /// Set the HTTP response body /// /// Body string content (default is "") public HttpResponse SetBody(string body = "") => SetBody(body.AsSpan()); /// /// Set the HTTP response body /// /// Body string content as a span of characters public HttpResponse 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 response body _cache.Append(body); _bodyIndex = index; _bodySize = length; _bodyLength = length; _bodyLengthProvided = true; return this; } /// /// Set the HTTP response body /// /// Body binary content public HttpResponse SetBody(byte[] body) => SetBody(body.AsSpan()); /// /// Set the HTTP response body /// /// Body binary content as a span of bytes public HttpResponse 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 response body _cache.Append(body); _bodyIndex = index; _bodySize = body.Length; _bodyLength = body.Length; _bodyLengthProvided = true; return this; } /// /// Set the HTTP response body length /// /// Body length 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; } /// /// Make OK response /// /// OK status (default is 200 (OK)) public HttpResponse MakeOkResponse(int status = 200) { Clear(); SetBegin(status); SetBody(); return this; } /// /// Make ERROR response /// /// Error content (default is "") /// Error content type (default is "text/plain; charset=UTF-8") public HttpResponse MakeErrorResponse(string content = "", string contentType = "text/plain; charset=UTF-8") { return MakeErrorResponse(500, content, contentType); } /// /// Make ERROR response /// /// Error status /// Error content (default is "") /// Error content type (default is "text/plain; charset=UTF-8") 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; } /// /// Make HEAD response /// public HttpResponse MakeHeadResponse() { Clear(); SetBegin(200); SetBody(); return this; } /// /// Make GET response /// /// String content (default is "") /// Content type (default is "text/plain; charset=UTF-8") public HttpResponse MakeGetResponse(string content = "", string contentType = "text/plain; charset=UTF-8") => MakeGetResponse(content.AsSpan(), contentType); /// /// Make GET response /// /// String content as a span of characters /// Content type (default is "text/plain; charset=UTF-8") public HttpResponse MakeGetResponse(ReadOnlySpan content, string contentType = "text/plain; charset=UTF-8") { Clear(); SetBegin(200); if (!string.IsNullOrEmpty(contentType)) SetHeader("Content-Type", contentType); SetBody(content); return this; } /// /// Make GET response /// /// Binary content /// Content type (default is "") public HttpResponse MakeGetResponse(byte[] content, string contentType = "") => MakeGetResponse(content.AsSpan(), contentType); /// /// Make GET response /// /// Binary content as a span of bytes /// Content type (default is "") public HttpResponse MakeGetResponse(ReadOnlySpan content, string contentType = "") { Clear(); SetBegin(200); if (!string.IsNullOrEmpty(contentType)) SetHeader("Content-Type", contentType); SetBody(content); return this; } /// /// Make OPTIONS response /// /// Allow methods (default is "HEAD,GET,POST,PUT,DELETE,OPTIONS,TRACE") public HttpResponse MakeOptionsResponse(string allow = "HEAD,GET,POST,PUT,DELETE,OPTIONS,TRACE") { Clear(); SetBegin(200); SetHeader("Allow", allow); SetBody(); return this; } /// /// Make TRACE response /// /// String content public HttpResponse MakeTraceResponse(string content) => MakeTraceResponse(content.AsSpan()); /// /// Make TRACE response /// /// String content as a span of characters public HttpResponse MakeTraceResponse(ReadOnlySpan content) { Clear(); SetBegin(200); SetHeader("Content-Type", "message/http"); SetBody(content); return this; } /// /// Make TRACE response /// /// Binary content public HttpResponse MakeTraceResponse(byte[] content) => MakeTraceResponse(content.AsSpan()); /// /// Make TRACE response /// /// Binary content as a span of bytes public HttpResponse MakeTraceResponse(ReadOnlySpan content) { Clear(); SetBegin(200); SetHeader("Content-Type", "message/http"); SetBody(content); return this; } /// /// Make TRACE response /// /// HTTP request 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 _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; } } }