using System; using System.Collections.Specialized; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; namespace OSHttpServer { public class HttpResponse : IHttpResponse { private const string DefaultContentType = "text/html;charset=UTF-8"; private readonly IHttpClientContext m_context; private readonly ResponseCookies m_cookies = new ResponseCookies(); private readonly NameValueCollection m_headers = new NameValueCollection(); private string m_httpVersion; private Stream m_body; private long m_contentLength; private string m_contentType; private Encoding m_encoding = Encoding.UTF8; private int m_keepAlive = 60; public uint requestID { get; private set; } public byte[] RawBuffer { get; set; } public int RawBufferStart { get; set; } public int RawBufferLen { get; set; } internal byte[] m_headerBytes = null; /// /// Initializes a new instance of the class. /// /// Client that send the . /// Contains information of what the client want to receive. /// cannot be empty. public HttpResponse(IHttpClientContext context, IHttpRequest request) { m_httpVersion = request.HttpVersion; if (string.IsNullOrEmpty(m_httpVersion)) m_httpVersion = "HTTP/1.0"; Status = HttpStatusCode.OK; m_context = context; m_Connetion = request.Connection; requestID = request.ID; RawBufferStart = -1; RawBufferLen = -1; } /// /// Initializes a new instance of the class. /// /// Client that send the . /// Version of HTTP protocol that the client uses. /// Type of HTTP connection used. internal HttpResponse(IHttpClientContext context, string httpVersion, ConnectionType connectionType) { Status = HttpStatusCode.OK; m_context = context; m_httpVersion = httpVersion; m_Connetion = connectionType; } private ConnectionType m_Connetion; public ConnectionType Connection { get { return m_Connetion; } set { return; } } private int m_priority = 0; public int Priority { get { return m_priority;} set { m_priority = (value > 0 && m_priority < 3)? value : 0;} } #region IHttpResponse Members /// /// The body stream is used to cache the body contents /// before sending everything to the client. It's the simplest /// way to serve documents. /// public Stream Body { get { if(m_body == null) m_body = new MemoryStream(); return m_body; } set { m_body = value; } } /// /// The chunked encoding modifies the body of a message in order to /// transfer it as a series of chunks, each with its own size indicator, /// followed by an OPTIONAL trailer containing entity-header fields. This /// allows dynamically produced content to be transferred along with the /// information necessary for the recipient to verify that it has /// received the full message. /// public bool Chunked { get; set; } /// /// Defines the version of the HTTP Response for applications where it's required /// for this to be forced. /// public string ProtocolVersion { get { return m_httpVersion; } set { m_httpVersion = value; } } /// /// Encoding to use when sending stuff to the client. /// /// Default is UTF8 public Encoding Encoding { get { return m_encoding; } set { m_encoding = value; } } /// /// Number of seconds to keep connection alive /// /// Only used if Connection property is set to . public int KeepAlive { get { return m_keepAlive; } set { if (value > 400) m_keepAlive = 400; else if (value <= 0) m_keepAlive = 0; else m_keepAlive = value; } } /// /// Status code that is sent to the client. /// /// Default is public HttpStatusCode Status { get; set; } /// /// Information about why a specific status code was used. /// public string Reason { get; set; } /// /// Size of the body. MUST be specified before sending the header, /// unless property Chunked is set to true. /// public long ContentLength { get { return m_contentLength; } set { m_contentLength = value; } } /// /// Kind of content in the body /// /// Default type is "text/html" public string ContentType { get { return m_contentType; } set { m_contentType = value; } } /// /// Headers have been sent to the client- /// /// You can not send any additional headers if they have already been sent. public bool HeadersSent { get; private set; } /// /// The whole response have been sent. /// public bool Sent { get; private set; } /// /// Cookies that should be created/changed. /// public ResponseCookies Cookies { get { return m_cookies; } } /// /// Add another header to the document. /// /// Name of the header, case sensitive, use lower cases. /// Header values can span over multiple lines as long as each line starts with a white space. New line chars should be \r\n /// If headers already been sent. /// If value conditions have not been met. /// Adding any header will override the default ones and those specified by properties. public void AddHeader(string name, string value) { if (HeadersSent) throw new InvalidOperationException("Headers have already been sent."); for (int i = 1; i < value.Length; ++i) { if (value[i] == '\r' && !char.IsWhiteSpace(value[i - 1])) throw new ArgumentException("New line in value do not start with a white space."); if (value[i] == '\n' && value[i - 1] != '\r') throw new ArgumentException("Invalid new line sequence, should be \\r\\n (crlf)."); } m_headers[name] = value; } /// /// Send headers and body to the browser. /// /// If content have already been sent. public void SendOri() { if (Sent) throw new InvalidOperationException("Everything have already been sent."); m_context.ReqResponseAboutToSend(requestID); if (m_context.MAXRequests == 0 || m_keepAlive == 0) { Connection = ConnectionType.Close; m_context.TimeoutKeepAlive = 0; } else { if (m_keepAlive > 0) m_context.TimeoutKeepAlive = m_keepAlive * 1000; } if (!HeadersSent) { if (!SendHeaders()) { m_body.Dispose(); Sent = true; return; } } if(RawBuffer != null) { if(RawBufferStart >= 0 && RawBufferLen > 0) { if (RawBufferStart > RawBuffer.Length) RawBufferStart = 0; if (RawBufferLen + RawBufferStart > RawBuffer.Length) RawBufferLen = RawBuffer.Length - RawBufferStart; /* int curlen; while(RawBufferLen > 0) { curlen = RawBufferLen; if(curlen > 8192) curlen = 8192; if (!_context.Send(RawBuffer, RawBufferStart, curlen)) { RawBuffer = null; RawBufferStart = -1; RawBufferLen = -1; Body.Dispose(); return; } RawBufferLen -= curlen; RawBufferStart += curlen; } */ if(RawBufferLen > 0) { if (!m_context.Send(RawBuffer, RawBufferStart, RawBufferLen)) { RawBuffer = null; RawBufferStart = -1; RawBufferLen = -1; if(m_body != null) m_body.Dispose(); Sent = true; return; } } } RawBuffer = null; RawBufferStart = -1; RawBufferLen = -1; } if(m_body != null && m_body.Length > 0) { m_body.Flush(); m_body.Seek(0, SeekOrigin.Begin); var buffer = new byte[8192]; int bytesRead = m_body.Read(buffer, 0, 8192); while (bytesRead > 0) { if (!m_context.Send(buffer, 0, bytesRead)) break; bytesRead = m_body.Read(buffer, 0, 8192); } m_body.Dispose(); } Sent = true; m_context.ReqResponseSent(requestID, Connection); } /// /// Make sure that you have specified and sent the headers first. /// /// /// If headers have not been sent. /// /// offset of first byte to send /// number of bytes to send. /// /// /// This method can be used if you want to send body contents without caching them first. This /// is recommended for larger files to keep the memory usage low. public bool SendBody(byte[] buffer, int offset, int count) { if (!HeadersSent) throw new InvalidOperationException("Send headers, and remember to specify ContentLength first."); bool sent = m_context.Send(buffer, offset, count); Sent = true; if (sent) m_context.ReqResponseSent(requestID, Connection); return sent; } /// /// Make sure that you have specified and sent the headers first. /// /// /// If headers have not been sent. /// /// /// /// This method can be used if you want to send body contents without caching them first. This /// is recommended for larger files to keep the memory usage low. public bool SendBody(byte[] buffer) { if (!HeadersSent) throw new InvalidOperationException("Send headers, and remember to specify ContentLength first."); bool sent = m_context.Send(buffer); if (sent) m_context.ReqResponseSent(requestID, Connection); Sent = true; return sent; } /// /// Send headers to the client. /// /// If headers already been sent. /// /// /// public bool SendHeaders() { if (HeadersSent) throw new InvalidOperationException("Header have already been sent."); HeadersSent = true; if (m_headers["Date"] == null) m_headers["Date"] = DateTime.Now.ToString("r"); if (m_headers["Content-Length"] == null) { int len = (int)m_contentLength; if(len == 0) { if(m_body != null) len = (int)m_body.Length; if(RawBuffer != null) len += RawBufferLen; } m_headers["Content-Length"] = len.ToString(); } if (m_headers["Content-Type"] == null) m_headers["Content-Type"] = m_contentType ?? DefaultContentType; if (m_headers["Server"] == null) m_headers["Server"] = "Tiny WebServer"; int keepaliveS = m_context.TimeoutKeepAlive / 1000; if (Connection == ConnectionType.KeepAlive && keepaliveS > 0 && m_context.MAXRequests > 0) { m_headers["Keep-Alive"] = "timeout=" + keepaliveS + ", max=" + m_context.MAXRequests; m_headers["Connection"] = "Keep-Alive"; } else m_headers["Connection"] = "close"; var sb = new StringBuilder(); sb.AppendFormat("{0} {1} {2}\r\n", m_httpVersion, (int)Status, string.IsNullOrEmpty(Reason) ? Status.ToString() : Reason); for (int i = 0; i < m_headers.Count; ++i) { string headerName = m_headers.AllKeys[i]; string[] values = m_headers.GetValues(i); if (values == null) continue; foreach (string value in values) sb.AppendFormat("{0}: {1}\r\n", headerName, value); } foreach (ResponseCookie cookie in Cookies) sb.AppendFormat("Set-Cookie: {0}\r\n", cookie); sb.Append("\r\n"); m_headers.Clear(); return m_context.Send(Encoding.GetBytes(sb.ToString())); } public byte[] GetHeaders() { HeadersSent = true; var sb = new StringBuilder(); if(string.IsNullOrWhiteSpace(m_httpVersion)) sb.AppendFormat("HTTP1/0 {0} {1}\r\n", (int)Status, string.IsNullOrEmpty(Reason) ? Status.ToString() : Reason); else sb.AppendFormat("{0} {1} {2}\r\n", m_httpVersion, (int)Status, string.IsNullOrEmpty(Reason) ? Status.ToString() : Reason); if (m_headers["Date"] == null) sb.AppendFormat("Date: {0}\r\n", DateTime.Now.ToString("r")); if (m_headers["Content-Length"] == null) { long len = m_contentLength; if (len == 0) { len = Body.Length; if (RawBuffer != null && RawBufferLen > 0) len += RawBufferLen; } sb.AppendFormat("Content-Length: {0}\r\n", len); } if (m_headers["Content-Type"] == null) sb.AppendFormat("Content-Type: {0}\r\n", m_contentType ?? DefaultContentType); if (m_headers["Server"] == null) sb.Append("Server: OSWebServer\r\n"); int keepaliveS = m_context.TimeoutKeepAlive / 1000; if (Connection == ConnectionType.KeepAlive && keepaliveS > 0 && m_context.MAXRequests > 0) { sb.AppendFormat("Keep-Alive:timeout={0}, max={1}\r\n", keepaliveS, m_context.MAXRequests); sb.Append("Connection: Keep-Alive\r\n"); } else sb.Append("Connection: close\r\n"); if (m_headers["Connection"] != null) m_headers["Connection"] = null; if (m_headers["Keep-Alive"] != null) m_headers["Keep-Alive"] = null; for (int i = 0; i < m_headers.Count; ++i) { string headerName = m_headers.AllKeys[i]; string[] values = m_headers.GetValues(i); if (values == null) continue; foreach (string value in values) sb.AppendFormat("{0}: {1}\r\n", headerName, value); } foreach (ResponseCookie cookie in Cookies) sb.AppendFormat("Set-Cookie: {0}\r\n", cookie); sb.Append("\r\n"); m_headers.Clear(); return Encoding.GetBytes(sb.ToString()); } public void Send() { if (Sent) throw new InvalidOperationException("Everything have already been sent."); if (m_context.MAXRequests == 0 || m_keepAlive == 0) { Connection = ConnectionType.Close; m_context.TimeoutKeepAlive = 0; } else { if (m_keepAlive > 0) m_context.TimeoutKeepAlive = m_keepAlive * 1000; } m_headerBytes = GetHeaders(); if (RawBuffer != null) { if (RawBufferStart < 0 || RawBufferStart > RawBuffer.Length) return; if (RawBufferLen < 0) RawBufferLen = RawBuffer.Length; if (RawBufferLen + RawBufferStart > RawBuffer.Length) RawBufferLen = RawBuffer.Length - RawBufferStart; int tlen = m_headerBytes.Length + RawBufferLen; if(RawBufferLen > 0 && tlen < 16384) { byte[] tmp = new byte[tlen]; Array.Copy(m_headerBytes, tmp, m_headerBytes.Length); Array.Copy(RawBuffer, RawBufferStart, tmp, m_headerBytes.Length, RawBufferLen); m_headerBytes = null; RawBuffer = tmp; RawBufferStart = 0; RawBufferLen = tlen; } } m_context.StartSendResponse(this); } public async Task SendNextAsync(int bytesLimit) { if (m_headerBytes != null) { if(!await m_context.SendAsync(m_headerBytes, 0, m_headerBytes.Length).ConfigureAwait(false)) { if(m_body != null) m_body.Dispose(); RawBuffer = null; Sent = true; return; } bytesLimit -= m_headerBytes.Length; m_headerBytes = null; if(bytesLimit <= 0) { m_context.ContinueSendResponse(); return; } } if (RawBuffer != null) { if (RawBufferLen > 0) { bool sendRes; if(RawBufferLen > bytesLimit) { sendRes = await m_context.SendAsync(RawBuffer, RawBufferStart, bytesLimit).ConfigureAwait(false); RawBufferLen -= bytesLimit; RawBufferStart += bytesLimit; } else { sendRes = await m_context.SendAsync(RawBuffer, RawBufferStart, RawBufferLen).ConfigureAwait(false); RawBufferLen = 0; } if (!sendRes) { RawBuffer = null; if(m_body != null) Body.Dispose(); Sent = true; return; } } if (RawBufferLen <= 0) RawBuffer = null; else { m_context.ContinueSendResponse(); return; } } if (m_body != null && m_body.Length != 0) { m_body.Flush(); m_body.Seek(0, SeekOrigin.Begin); RawBuffer = new byte[m_body.Length]; RawBufferLen = m_body.Read(RawBuffer, 0, (int)m_body.Length); m_body.Dispose(); if(RawBufferLen > 0) { bool sendRes; if (RawBufferLen > bytesLimit) { sendRes = await m_context.SendAsync(RawBuffer, RawBufferStart, bytesLimit).ConfigureAwait(false); RawBufferLen -= bytesLimit; RawBufferStart += bytesLimit; } else { sendRes = await m_context.SendAsync(RawBuffer, RawBufferStart, RawBufferLen).ConfigureAwait(false); RawBufferLen = 0; } if (!sendRes) { RawBuffer = null; Sent = true; return; } } if (RawBufferLen > 0) { m_context.ContinueSendResponse(); return; } } if (m_body != null) m_body.Dispose(); Sent = true; m_context.ReqResponseSent(requestID, Connection); } /// /// Redirect client to somewhere else using the 302 status code. /// /// Destination of the redirect /// If headers already been sent. /// You can not do anything more with the request when a redirect have been done. This should be your last /// action. public void Redirect(Uri uri) { Status = HttpStatusCode.Redirect; m_headers["location"] = uri.ToString(); } /// /// redirect to somewhere /// /// where the redirect should go /// /// No body are allowed when doing redirects. /// public void Redirect(string url) { Status = HttpStatusCode.Redirect; m_headers["location"] = url; } public void Clear() { if(Body != null && Body.CanRead) Body.Dispose(); } #endregion } }