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;
}
}
}