using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading.Tasks; using OSHttpServer.Exceptions; using OSHttpServer.Parser; using System.Net.Security; using System.Security.Cryptography.X509Certificates; namespace OSHttpServer { /// /// Contains a connection to a browser/client. /// /// /// Remember to after you have hooked the event. /// public class HttpClientContext : IHttpClientContext, IDisposable { const int MAXREQUESTS = 20; const int MAXKEEPALIVE = 120000; static private int basecontextID; Queue m_requests; object m_requestsLock = new object(); public int m_maxRequests = MAXREQUESTS; public bool m_waitingResponse; private readonly byte[] m_ReceiveBuffer; private int m_ReceiveBytesLeft; private ILogWriter m_log; private readonly IHttpRequestParser m_parser; private Socket m_sock; public bool Available = true; public bool StreamPassedOff = false; public int LastActivityTimeMS = 0; public int MonitorKeepaliveStartMS = 0; public bool TriggerKeepalive = false; public int TimeoutFirstLine = 10000; // 10 seconds public int TimeoutRequestReceived = 30000; // 30 seconds public int TimeoutMaxIdle = 180000; // 3 minutes public int m_TimeoutKeepAlive = 30000; public bool FirstRequestLineReceived; public bool FullRequestReceived; private bool isSendingResponse = false; private bool m_isClosing = false; private HttpRequest m_currentRequest; private HttpResponse m_currentResponse; public int contextID { get; private set; } public int TimeoutKeepAlive { get { return m_TimeoutKeepAlive; } set { m_TimeoutKeepAlive = (value > MAXKEEPALIVE) ? MAXKEEPALIVE : value; } } public bool IsClosing { get { return m_isClosing;} } public int MaxRequests { get { return m_maxRequests; } set { if(value <= 1) m_maxRequests = 1; else m_maxRequests = value > MAXREQUESTS ? MAXREQUESTS : value; } } public bool IsSending() { return isSendingResponse; } public bool StopMonitoring; public IPEndPoint LocalIPEndPoint {get; set;} /// /// Initializes a new instance of the class. /// /// true if the connection is secured (SSL/TLS) /// client that connected. /// Stream used for communication /// Used to create a . /// Size of buffer to use when reading data. Must be at least 4096 bytes. /// If fails /// Stream must be writable and readable. public HttpClientContext(bool secured, IPEndPoint remoteEndPoint, Stream stream, ILogWriter m_logWriter, Socket sock) { if (!stream.CanWrite || !stream.CanRead) throw new ArgumentException("Stream must be writable and readable."); LocalIPEndPoint = remoteEndPoint; m_log = m_logWriter; m_isClosing = false; m_parser = new HttpRequestParser(m_log); m_parser.RequestCompleted += OnRequestCompleted; m_parser.RequestLineReceived += OnRequestLine; m_parser.HeaderReceived += OnHeaderReceived; m_parser.BodyBytesReceived += OnBodyBytesReceived; m_currentRequest = new HttpRequest(this); IsSecured = secured; m_stream = stream; m_sock = sock; m_ReceiveBuffer = new byte[16384]; m_requests = new Queue(); SSLCommonName = ""; if (secured) { SslStream _ssl = (SslStream)m_stream; X509Certificate _cert1 = _ssl.RemoteCertificate; if (_cert1 != null) { X509Certificate2 _cert2 = new X509Certificate2(_cert1); if (_cert2 != null) SSLCommonName = _cert2.GetNameInfo(X509NameType.SimpleName, false); } } ++basecontextID; if (basecontextID <= 0) basecontextID = 1; contextID = basecontextID; sock.NoDelay = true; } public bool CanSend() { if (contextID < 0 || m_isClosing) return false; if (m_stream == null || m_sock == null || !m_sock.Connected) return false; return true; } /// /// Process incoming body bytes. /// /// /// Bytes protected virtual void OnBodyBytesReceived(object sender, BodyEventArgs e) { m_currentRequest.AddToBody(e.Buffer, e.Offset, e.Count); } /// /// /// /// /// protected virtual void OnHeaderReceived(object sender, HeaderEventArgs e) { if (string.Compare(e.Name, "expect", true) == 0 && e.Value.Contains("100-continue")) { lock (m_requestsLock) { if (m_maxRequests == MAXREQUESTS) Respond("HTTP/1.1", HttpStatusCode.Continue, null); } } m_currentRequest.AddHeader(e.Name, e.Value); } private void OnRequestLine(object sender, RequestLineEventArgs e) { m_currentRequest.Method = e.HttpMethod; m_currentRequest.HttpVersion = e.HttpVersion; m_currentRequest.UriPath = e.UriPath; m_currentRequest.AddHeader("remote_addr", LocalIPEndPoint.Address.ToString()); m_currentRequest.AddHeader("remote_port", LocalIPEndPoint.Port.ToString()); m_currentRequest.ArrivalTS = ContextTimeoutManager.GetTimeStamp(); FirstRequestLineReceived = true; TriggerKeepalive = false; MonitorKeepaliveStartMS = 0; LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount(); } /// /// Start reading content. /// /// /// Make sure to call base.Start() if you override this method. /// public virtual void Start() { Task tk = new Task(() => ReceiveLoop()); tk.Start(); } /// /// Clean up context. /// /// /// public virtual void Cleanup() { if (StreamPassedOff) return; contextID = -100; if (m_stream != null) { m_stream.Close(); m_stream = null; m_sock = null; } m_currentRequest?.Clear(); m_currentRequest = null; m_currentResponse?.Clear(); m_currentResponse = null; if(m_requests != null) { while(m_requests.Count > 0) { HttpRequest req = m_requests.Dequeue(); req.Clear(); } } m_requests.Clear(); m_requests = null; m_parser.Clear(); FirstRequestLineReceived = false; FullRequestReceived = false; LastActivityTimeMS = 0; StopMonitoring = true; MonitorKeepaliveStartMS = 0; TriggerKeepalive = false; isSendingResponse = false; m_ReceiveBytesLeft = 0; } public void Close() { Dispose(); } /// /// Using SSL or other encryption method. /// [Obsolete("Use IsSecured instead.")] public bool Secured { get { return IsSecured; } } /// /// Using SSL or other encryption method. /// public bool IsSecured { get; internal set; } // returns the SSL commonName of remote Certificate public string SSLCommonName { get; internal set; } /// /// Specify which logger to use. /// public ILogWriter LogWriter { get { return m_log; } set { m_log = value ?? NullLogWriter.Instance; m_parser.LogWriter = m_log; } } private Stream m_stream; /// /// Gets or sets the network stream. /// internal Stream Stream { get { return m_stream; } set { m_stream = value; } } /// /// Disconnect from client /// /// error to report in the event. public void Disconnect(SocketError error) { // disconnect may not throw any exceptions try { try { if (m_stream != null) { if (error == SocketError.Success) { try { m_stream.Flush(); } catch { } } m_stream.Close(); m_stream = null; } m_sock = null; } catch { } Disconnected?.Invoke(this, new DisconnectedEventArgs(error)); } catch (Exception err) { LogWriter.Write(this, LogPrio.Error, "Disconnect threw an exception: " + err); } } private async void ReceiveLoop() { m_ReceiveBytesLeft = 0; try { while(true) { if (m_stream == null || !m_stream.CanRead) return; int bytesRead = await m_stream.ReadAsync(m_ReceiveBuffer, m_ReceiveBytesLeft, m_ReceiveBuffer.Length - m_ReceiveBytesLeft).ConfigureAwait(false); if (bytesRead == 0) { Disconnect(SocketError.Success); return; } if(m_isClosing) continue; m_ReceiveBytesLeft += bytesRead; int offset = m_parser.Parse(m_ReceiveBuffer, 0, m_ReceiveBytesLeft); if (m_stream == null) return; // "Connection: Close" in effect. while (offset != 0) { int nextBytesleft = m_ReceiveBytesLeft - offset; if(nextBytesleft <= 0) break; int nextOffset = m_parser.Parse(m_ReceiveBuffer, offset, nextBytesleft); if (m_stream == null) return; // "Connection: Close" in effect. if (nextOffset == 0) break; offset = nextOffset; } // copy unused bytes to the beginning of the array if (offset > 0 && m_ReceiveBytesLeft > offset) Buffer.BlockCopy(m_ReceiveBuffer, offset, m_ReceiveBuffer, 0, m_ReceiveBytesLeft - offset); m_ReceiveBytesLeft -= offset; if (StreamPassedOff) return; //? } } catch (BadRequestException err) { LogWriter.Write(this, LogPrio.Warning, "Bad request, responding with it. Error: " + err); try { Respond("HTTP/1.1", HttpStatusCode.BadRequest, err.Message); } catch (Exception err2) { LogWriter.Write(this, LogPrio.Fatal, "Failed to reply to a bad request. " + err2); } //Disconnect(SocketError.NoRecovery); Disconnect(SocketError.Success); // try to flush } catch (IOException err) { LogWriter.Write(this, LogPrio.Debug, "Failed to end receive: " + err.Message); if (err.InnerException is SocketException) Disconnect((SocketError)((SocketException)err.InnerException).ErrorCode); else Disconnect(SocketError.ConnectionReset); } catch (ObjectDisposedException err) { LogWriter.Write(this, LogPrio.Debug, "Failed to end receive : " + err.Message); Disconnect(SocketError.NotSocket); } catch (NullReferenceException err) { LogWriter.Write(this, LogPrio.Debug, "Failed to end receive : NullRef: " + err.Message); Disconnect(SocketError.NoRecovery); } catch (Exception err) { LogWriter.Write(this, LogPrio.Debug, "Failed to end receive: " + err.Message); Disconnect(SocketError.NoRecovery); } } private void OnRequestCompleted(object source, EventArgs args) { TriggerKeepalive = false; MonitorKeepaliveStartMS = 0; FullRequestReceived = true; LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount(); if (m_maxRequests == 0) return; if (--m_maxRequests == 0) m_currentRequest.Connection = ConnectionType.Close; if(m_currentRequest.Uri == null) { // should not happen try { Uri uri = new Uri(m_currentRequest.Secure ? "https://" : "http://" + m_currentRequest.UriPath); m_currentRequest.Uri = uri; m_currentRequest.UriPath = uri.AbsolutePath; } catch { return; } } // load cookies if they exist if(m_currentRequest.Headers["cookie"] != null) m_currentRequest.SetCookies(new RequestCookies(m_currentRequest.Headers["cookie"])); m_currentRequest.Body.Seek(0, SeekOrigin.Begin); bool donow = true; lock (m_requestsLock) { if(m_waitingResponse) { m_requests.Enqueue(m_currentRequest); donow = false; } else m_waitingResponse = true; } if(donow) RequestReceived?.Invoke(this, new RequestEventArgs(m_currentRequest)); m_currentRequest = new HttpRequest(this); } public void StartSendResponse(HttpResponse response) { LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount(); isSendingResponse = true; m_currentResponse = response; ContextTimeoutManager.EnqueueSend(this, response.Priority); } public bool TrySendResponse(int bytesLimit) { if(m_currentResponse == null) return false; if (m_currentResponse.Sent) return false; if(!CanSend()) return false; LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount(); m_currentResponse?.SendNextAsync(bytesLimit); return false; } public void ContinueSendResponse(bool notThrottled) { if(m_currentResponse == null) return; ContextTimeoutManager.EnqueueSend(this, m_currentResponse.Priority, notThrottled); } public void EndSendResponse(uint requestID, ConnectionType ctype) { isSendingResponse = false; m_currentResponse?.Clear(); m_currentResponse = null; lock (m_requestsLock) m_waitingResponse = false; if(contextID < 0) return; if (ctype == ConnectionType.Close) { m_isClosing = true; m_requests.Clear(); TriggerKeepalive = true; return; } else { LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount(); if (Stream == null || !Stream.CanWrite) return; HttpRequest nextRequest = null; lock (m_requestsLock) { if (m_requests != null && m_requests.Count > 0) nextRequest = m_requests.Dequeue(); if (nextRequest != null && RequestReceived != null) { m_waitingResponse = true; TriggerKeepalive = false; } else TriggerKeepalive = true; } if (nextRequest != null) RequestReceived?.Invoke(this, new RequestEventArgs(nextRequest)); } } /// /// Send a response. /// /// Either or /// HTTP status code /// reason for the status code. /// HTML body contents, can be null or empty. /// A content type to return the body as, i.e. 'text/html' or 'text/plain', defaults to 'text/html' if null or empty /// If is invalid. public void Respond(string httpVersion, HttpStatusCode statusCode, string reason, string body, string contentType) { LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount(); if (string.IsNullOrEmpty(reason)) reason = statusCode.ToString(); byte[] buffer; if(string.IsNullOrEmpty(body)) buffer = Encoding.ASCII.GetBytes(httpVersion + " " + (int)statusCode + " " + reason + "\r\n\r\n"); else { if (string.IsNullOrEmpty(contentType)) contentType = "text/html"; buffer = Encoding.UTF8.GetBytes( string.Format("{0} {1} {2}\r\nContent-Type: {5}\r\nContent-Length: {3}\r\n\r\n{4}", httpVersion, (int)statusCode, reason ?? statusCode.ToString(), body.Length, body, contentType)); } Send(buffer); } /// /// Send a response. /// /// Either or /// HTTP status code /// reason for the status code. public void Respond(string httpVersion, HttpStatusCode statusCode, string reason) { if (string.IsNullOrEmpty(reason)) reason = statusCode.ToString(); byte[] buffer = Encoding.ASCII.GetBytes(httpVersion + " " + (int)statusCode + " " + reason + "\r\n\r\n"); Send(buffer); } /// /// send a whole buffer /// /// buffer to send /// public bool Send(byte[] buffer) { if (buffer == null) throw new ArgumentNullException("buffer"); return Send(buffer, 0, buffer.Length); } /// /// Send data using the stream /// /// Contains data to send /// Start position in buffer /// number of bytes to send /// /// private object sendLock = new object(); public bool Send(byte[] buffer, int offset, int size) { if (m_stream == null || m_sock == null || !m_sock.Connected) return false; if (offset + size > buffer.Length) throw new ArgumentOutOfRangeException("offset", offset, "offset + size is beyond end of buffer."); LastActivityTimeMS = ContextTimeoutManager.EnvironmentTickCount(); bool ok = true; ContextTimeoutManager.ContextEnterActiveSend(); lock (sendLock) // can't have overlaps here { try { m_stream.Write(buffer, offset, size); } catch { ok = false; } } ContextTimeoutManager.ContextLeaveActiveSend(); if (!ok && m_stream != null) Disconnect(SocketError.NoRecovery); return ok; } public async Task SendAsync(byte[] buffer, int offset, int size) { if (m_stream == null || m_sock == null || !m_sock.Connected) return false; if (offset + size > buffer.Length) throw new ArgumentOutOfRangeException("offset", offset, "offset + size is beyond end of buffer."); bool ok = true; ContextTimeoutManager.ContextEnterActiveSend(); try { await m_stream.WriteAsync(buffer, offset, size).ConfigureAwait(false); } catch { ok = false; } ContextTimeoutManager.ContextLeaveActiveSend(); if (!ok && m_stream != null) Disconnect(SocketError.NoRecovery); return ok; } /// /// The context have been disconnected. /// /// /// Event can be used to clean up a context, or to reuse it. /// public event EventHandler Disconnected; /// /// A request have been received in the context. /// public event EventHandler RequestReceived; public HTTPNetworkContext GiveMeTheNetworkStreamIKnowWhatImDoing() { StreamPassedOff = true; m_parser.RequestCompleted -= OnRequestCompleted; m_parser.RequestLineReceived -= OnRequestLine; m_parser.HeaderReceived -= OnHeaderReceived; m_parser.BodyBytesReceived -= OnBodyBytesReceived; m_parser.Clear(); m_currentRequest?.Clear(); m_currentRequest = null; m_currentResponse?.Clear(); m_currentResponse = null; if (m_requests != null) { while (m_requests.Count > 0) { HttpRequest req = m_requests.Dequeue(); req.Clear(); } } m_requests.Clear(); m_requests = null; return new HTTPNetworkContext() { Socket = m_sock, Stream = m_stream as NetworkStream }; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected void Dispose(bool disposing) { if (contextID >= 0) { StreamPassedOff = false; Cleanup(); } } } }