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 = 60000; static private int basecontextID; private readonly byte[] m_ReceiveBuffer; private int m_ReceiveBytesLeft; private ILogWriter _log; private readonly IHttpRequestParser m_parser; private readonly int m_bufferSize; private HashSet requestsInServiceIDs; private Socket m_sock; public bool Available = true; public bool StreamPassedOff = false; public int MonitorStartMS = 0; public int MonitorKeepaliveMS = 0; public bool TriggerKeepalive = false; public int TimeoutFirstLine = 70000; // 70 seconds public int TimeoutRequestReceived = 180000; // 180 seconds // The difference between this and request received is on POST more time is needed before we get the full request. public int TimeoutFullRequestProcessed = 600000; // 10 minutes public int m_TimeoutKeepAlive = MAXKEEPALIVE; // 400 seconds before keepalive timeout // public int TimeoutKeepAlive = 120000; // 400 seconds before keepalive timeout public int m_maxRequests = MAXREQUESTS; public bool FirstRequestLineReceived; public bool FullRequestReceived; public bool FullRequestProcessed; private bool isSendingResponse = 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 int MAXRequests { get { return m_maxRequests; } set { m_maxRequests = value > MAXREQUESTS ? MAXREQUESTS : value; } } public bool IsSending() { return isSendingResponse; } public bool StopMonitoring; /// /// Context have been started (a new client have connected) /// public event EventHandler Started; /// /// 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, IRequestParserFactory parserFactory, Socket sock) { if (!stream.CanWrite || !stream.CanRead) throw new ArgumentException("Stream must be writable and readable."); RemoteAddress = remoteEndPoint.Address.ToString(); RemotePort = remoteEndPoint.Port.ToString(); _log = NullLogWriter.Instance; m_parser = parserFactory.CreateParser(_log); m_parser.RequestCompleted += OnRequestCompleted; m_parser.RequestLineReceived += OnRequestLine; m_parser.HeaderReceived += OnHeaderReceived; m_parser.BodyBytesReceived += OnBodyBytesReceived; m_currentRequest = new HttpRequest(this); IsSecured = secured; _stream = stream; m_sock = sock; m_bufferSize = 8196; m_ReceiveBuffer = new byte[m_bufferSize]; requestsInServiceIDs = new HashSet(); SSLCommonName = ""; if (secured) { SslStream _ssl = (SslStream)_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; } public bool CanSend() { if (contextID < 0) return false; if (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 (requestsInServiceIDs) { if (requestsInServiceIDs.Count == 0) Respond("HTTP/1.1", HttpStatusCode.Continue, "Please continue."); } } 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", RemoteAddress); m_currentRequest.AddHeader("remote_port", RemotePort); FirstRequestLineReceived = true; TriggerKeepalive = false; MonitorKeepaliveMS = 0; } /// /// Start reading content. /// /// /// Make sure to call base.Start() if you override this method. /// public virtual void Start() { ReceiveLoop(); Started?.Invoke(this, EventArgs.Empty); } /// /// Clean up context. /// /// /// public virtual void Cleanup() { if (StreamPassedOff) return; if (Stream != null) { Stream.Close(); Stream = null; m_sock = null; } m_currentRequest?.Clear(); m_currentRequest = null; m_currentResponse?.Clear(); m_currentResponse = null; requestsInServiceIDs.Clear(); FirstRequestLineReceived = false; FullRequestReceived = false; FullRequestProcessed = false; MonitorStartMS = 0; StopMonitoring = true; MonitorKeepaliveMS = 0; TriggerKeepalive = false; isSendingResponse = false; m_ReceiveBytesLeft = 0; contextID = -100; m_parser.Clear(); } 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 _log; } set { _log = value ?? NullLogWriter.Instance; m_parser.LogWriter = _log; } } private Stream _stream; /// /// Gets or sets the network stream. /// internal Stream Stream { get { return _stream; } set { _stream = value; } } /// /// Gets or sets IP address that the client connected from. /// internal string RemoteAddress { get; set; } /// /// Gets or sets port that the client connected from. /// internal string RemotePort { get; set; } /// /// Disconnect from client /// /// error to report in the event. public void Disconnect(SocketError error) { // disconnect may not throw any exceptions try { try { if (Stream != null) { if (error == SocketError.Success) Stream.Flush(); Stream.Close(); 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 void OnReceive(IAsyncResult ar) { try { int bytesRead = 0; if (Stream == null) return; try { bytesRead = Stream.EndRead(ar); } catch (NullReferenceException) { Disconnect(SocketError.ConnectionReset); return; } if (bytesRead == 0) { Disconnect(SocketError.ConnectionReset); return; } m_ReceiveBytesLeft += bytesRead; if (m_ReceiveBytesLeft > m_ReceiveBuffer.Length) { throw new BadRequestException("HTTP header Too large: " + m_ReceiveBytesLeft); } int offset = m_parser.Parse(m_ReceiveBuffer, 0, m_ReceiveBytesLeft); if (Stream == null) return; // "Connection: Close" in effect. // try again to see if we can parse another message (check parser to see if it is looking for a new message) int nextOffset; int nextBytesleft = m_ReceiveBytesLeft - offset; while (offset != 0 && nextBytesleft > 0) { nextOffset = m_parser.Parse(m_ReceiveBuffer, offset, nextBytesleft); if (Stream == null) return; // "Connection: Close" in effect. if (nextOffset == 0) break; offset = nextOffset; nextBytesleft = m_ReceiveBytesLeft - offset; } // 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 (Stream != null && Stream.CanRead) { if (!StreamPassedOff) Stream.BeginRead(m_ReceiveBuffer, m_ReceiveBytesLeft, m_ReceiveBuffer.Length - m_ReceiveBytesLeft, OnReceive, null); else { _log.Write(this, LogPrio.Warning, "Could not read any more from the socket."); Disconnect(SocketError.Success); } } } catch (BadRequestException err) { LogWriter.Write(this, LogPrio.Warning, "Bad request, responding with it. Error: " + err); try { Respond("HTTP/1.0", HttpStatusCode.BadRequest, err.Message); } catch (Exception err2) { LogWriter.Write(this, LogPrio.Fatal, "Failed to reply to a bad request. " + err2); } Disconnect(SocketError.NoRecovery); } 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 async void ReceiveLoop() { m_ReceiveBytesLeft = 0; try { while(true) { if (_stream == null || !_stream.CanRead) return; int bytesRead = await _stream.ReadAsync(m_ReceiveBuffer, m_ReceiveBytesLeft, m_ReceiveBuffer.Length - m_ReceiveBytesLeft).ConfigureAwait(false); if (bytesRead == 0) { Disconnect(SocketError.ConnectionReset); return; } m_ReceiveBytesLeft += bytesRead; if (m_ReceiveBytesLeft > m_ReceiveBuffer.Length) throw new BadRequestException("HTTP header Too large: " + m_ReceiveBytesLeft); int offset = m_parser.Parse(m_ReceiveBuffer, 0, m_ReceiveBytesLeft); if (Stream == null) return; // "Connection: Close" in effect. // try again to see if we can parse another message (check parser to see if it is looking for a new message) int nextOffset; int nextBytesleft = m_ReceiveBytesLeft - offset; while (offset != 0 && nextBytesleft > 0) { nextOffset = m_parser.Parse(m_ReceiveBuffer, offset, nextBytesleft); if (Stream == null) return; // "Connection: Close" in effect. if (nextOffset == 0) break; offset = nextOffset; nextBytesleft = m_ReceiveBytesLeft - offset; } // 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.0", HttpStatusCode.BadRequest, err.Message); } catch (Exception err2) { LogWriter.Write(this, LogPrio.Fatal, "Failed to reply to a bad request. " + err2); } Disconnect(SocketError.NoRecovery); } 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; MonitorKeepaliveMS = 0; // load cookies if they exist RequestCookies cookies = m_currentRequest.Headers["cookie"] != null ? new RequestCookies(m_currentRequest.Headers["cookie"]) : new RequestCookies(String.Empty); m_currentRequest.SetCookies(cookies); m_currentRequest.Body.Seek(0, SeekOrigin.Begin); FullRequestReceived = true; int nreqs; lock (requestsInServiceIDs) { nreqs = requestsInServiceIDs.Count; requestsInServiceIDs.Add(m_currentRequest.ID); if (m_maxRequests > 0) m_maxRequests--; } // for now pipeline requests need to be serialized by opensim RequestReceived(this, new RequestEventArgs(m_currentRequest)); m_currentRequest = new HttpRequest(this); int nreqsnow; lock (requestsInServiceIDs) { nreqsnow = requestsInServiceIDs.Count; } if (nreqs != nreqsnow) { // request was not done by us } } public void ReqResponseAboutToSend(uint requestID) { isSendingResponse = true; } public void StartSendResponse(HttpResponse response) { 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; m_currentResponse?.SendNextAsync(bytesLimit); return false; } public void ContinueSendResponse() { if(m_currentResponse == null) return; ContextTimeoutManager.EnqueueSend(this, m_currentResponse.Priority); } public void ReqResponseSent(uint requestID, ConnectionType ctype) { isSendingResponse = false; m_currentResponse?.Clear(); m_currentResponse = null; bool doclose = ctype == ConnectionType.Close; lock (requestsInServiceIDs) { requestsInServiceIDs.Remove(requestID); // doclose = doclose && requestsInServiceIDs.Count == 0; if (requestsInServiceIDs.Count > 1) { } } if (doclose) Disconnect(SocketError.Success); else { lock (requestsInServiceIDs) { if (requestsInServiceIDs.Count == 0) TriggerKeepalive = true; } } } /// /// 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) { if (string.IsNullOrEmpty(contentType)) contentType = "text/html"; if (string.IsNullOrEmpty(reason)) reason = statusCode.ToString(); string response = string.IsNullOrEmpty(body) ? httpVersion + " " + (int)statusCode + " " + reason + "\r\n\r\n" : 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); byte[] buffer = Encoding.ASCII.GetBytes(response); Send(buffer); if (m_currentRequest.Connection == ConnectionType.Close) FullRequestProcessed = true; } /// /// Send a response. /// /// Either or /// HTTP status code /// reason for the status code. public void Respond(string httpVersion, HttpStatusCode statusCode, string reason) { Respond(httpVersion, statusCode, reason, null, null); } /// /// 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 (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; lock (sendLock) // can't have overlaps here { try { Stream.Write(buffer, offset, size); } catch { ok = false; } if (!ok && Stream != null) Disconnect(SocketError.NoRecovery); return ok; } } public async Task SendAsync(byte[] buffer, int offset, int size) { if (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 Stream.WriteAsync(buffer, offset, size).ConfigureAwait(false); } catch { ok = false; } ContextTimeoutManager.ContextLeaveActiveSend(); if (!ok && 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 = delegate { }; /// /// A request have been received in the context. /// public event EventHandler RequestReceived = delegate { }; public HTTPNetworkContext GiveMeTheNetworkStreamIKnowWhatImDoing() { StreamPassedOff = true; m_parser.RequestCompleted -= OnRequestCompleted; m_parser.RequestLineReceived -= OnRequestLine; m_parser.HeaderReceived -= OnHeaderReceived; m_parser.BodyBytesReceived -= OnBodyBytesReceived; m_parser.Clear(); return new HTTPNetworkContext() { Socket = m_sock, Stream = _stream as NetworkStream }; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public void Dispose(bool disposing) { if (contextID >= 0) { StreamPassedOff = false; Cleanup(); } } } }