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 { public event EventHandler BandWitdhEvent; 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; } public double RequestTS { get; private 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(IHttpRequest request) { m_httpVersion = request.HttpVersion; if (string.IsNullOrEmpty(m_httpVersion)) m_httpVersion = "HTTP/1.1"; Status = HttpStatusCode.OK; m_context = request.Context; m_Connetion = request.Connection; requestID = request.ID; RequestTS = request.ArrivalTS; 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; } } /// /// 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, /// public long ContentLength { get { return m_contentLength; } set { m_contentLength = value; } } /// /// Kind of content /// /// 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; } public byte[] GetHeaders() { HeadersSent = true; var sb = new StringBuilder(); if(string.IsNullOrWhiteSpace(m_httpVersion)) sb.AppendFormat("HTTP/1.1 {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"); if(Status != HttpStatusCode.OK) { sb.Append("Connection: close\r\n"); Connection = ConnectionType.Close; } else { 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"); Connection = ConnectionType.Close; } } 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(m_context.IsClosing) return; 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; } if (RawBuffer != null) { if (RawBufferStart > RawBuffer.Length) return; if (RawBufferStart < 0) RawBufferStart = 0; if (RawBufferLen < 0) RawBufferLen = RawBuffer.Length; if (RawBufferLen + RawBufferStart > RawBuffer.Length) RawBufferLen = RawBuffer.Length - RawBufferStart; } m_headerBytes = GetHeaders(); if (RawBuffer != null) { 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_context.CanSend()) { m_context.ContinueSendResponse(true); return; } 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(true); return; } } if (RawBuffer != null) { if (RawBufferLen > 0) { if(BandWitdhEvent!=null) bytesLimit = CheckBandwidth(RawBufferLen, bytesLimit); bool sendRes; if(RawBufferLen > bytesLimit) { sendRes = (await m_context.SendAsync(RawBuffer, RawBufferStart, bytesLimit).ConfigureAwait(false)); if (sendRes) { RawBufferLen -= bytesLimit; RawBufferStart += bytesLimit; } } else { sendRes = await m_context.SendAsync(RawBuffer, RawBufferStart, RawBufferLen).ConfigureAwait(false); if(sendRes) RawBufferLen = 0; } if (!sendRes) { if (m_context.CanSend()) { m_context.ContinueSendResponse(true); return; } RawBuffer = null; if(m_body != null) Body.Dispose(); Sent = true; return; } } if (RawBufferLen <= 0) RawBuffer = null; else { m_context.ContinueSendResponse(true); return; } } if (m_body != null && m_body.Length != 0) { MemoryStream mb = m_body as MemoryStream; RawBuffer = mb.GetBuffer(); RawBufferStart = 0; // must be a internal buffer, or starting at 0 RawBufferLen = (int)mb.Length; mb.Dispose(); m_body = null; if(RawBufferLen > 0) { bool sendRes; if (RawBufferLen > bytesLimit) { sendRes = await m_context.SendAsync(RawBuffer, RawBufferStart, bytesLimit).ConfigureAwait(false); if (sendRes) { RawBufferLen -= bytesLimit; RawBufferStart += bytesLimit; } } else { sendRes = await m_context.SendAsync(RawBuffer, RawBufferStart, RawBufferLen).ConfigureAwait(false); if (sendRes) RawBufferLen = 0; } if (!sendRes) { if (m_context.CanSend()) { m_context.ContinueSendResponse(true); return; } RawBuffer = null; Sent = true; return; } } if (RawBufferLen > 0) { m_context.ContinueSendResponse(false); return; } } if (m_body != null) m_body.Dispose(); Sent = true; m_context.EndSendResponse(requestID, Connection); } private int CheckBandwidth(int request, int bytesLimit) { if(request > bytesLimit) request = bytesLimit; var args = new BandWitdhEventArgs(request); BandWitdhEvent?.Invoke(this, args); if(args.Result > 8196) return args.Result; return 8196; } public void Clear() { if(Body != null && Body.CanRead) Body.Dispose(); } #endregion } }