using System; using System.Text; using OSHttpServer.Exceptions; namespace OSHttpServer.Parser { /// /// Parses a HTTP request directly from a stream /// public class HttpRequestParser : IHttpRequestParser { private ILogWriter m_log; private readonly BodyEventArgs m_bodyArgs = new BodyEventArgs(); private readonly HeaderEventArgs m_headerArgs = new HeaderEventArgs(); private readonly RequestLineEventArgs m_requestLineArgs = new RequestLineEventArgs(); private string m_curHeaderName = string.Empty; private string m_curHeaderValue = string.Empty; private int m_bodyBytesLeft; /// /// More body bytes have been received. /// public event EventHandler BodyBytesReceived; /// /// Request line have been received. /// public event EventHandler RequestLineReceived; /// /// A header have been received. /// public event EventHandler HeaderReceived; /// /// Create a new request parser /// /// delegate receiving log entries. public HttpRequestParser(ILogWriter logWriter) { m_log = logWriter ?? NullLogWriter.Instance; } /// /// Add a number of bytes to the body /// /// buffer containing more body bytes. /// starting offset in buffer /// number of bytes, from offset, to read. /// offset to continue from. private int AddToBody(byte[] buffer, int offset, int count) { // got all bytes we need, or just a few of them? int bytesUsed = count > m_bodyBytesLeft ? m_bodyBytesLeft : count; m_bodyArgs.Buffer = buffer; m_bodyArgs.Offset = offset; m_bodyArgs.Count = bytesUsed; BodyBytesReceived?.Invoke(this, m_bodyArgs); m_bodyBytesLeft -= bytesUsed; if (m_bodyBytesLeft == 0) { // got a complete request. m_log.Write(this, LogPrio.Trace, "Request parsed successfully."); OnRequestCompleted(); Clear(); } return offset + bytesUsed; } /// /// Remove all state information for the request. /// public void Clear() { m_bodyBytesLeft = 0; m_curHeaderName = string.Empty; m_curHeaderValue = string.Empty; CurrentState = RequestParserState.FirstLine; } /// /// Gets or sets the log writer. /// public ILogWriter LogWriter { get { return m_log; } set { m_log = value ?? NullLogWriter.Instance; } } /// /// Parse request line /// /// /// If line is incorrect /// Expects the following format: "Method SP Request-URI SP HTTP-Version CRLF" protected void OnFirstLine(string value) { // //todo: In the interest of robustness, servers SHOULD ignore any empty line(s) received where a Request-Line is expected. // In other words, if the server is reading the protocol stream at the beginning of a message and receives a CRLF first, it should ignore the CRLF. // m_log.Write(this, LogPrio.Debug, "Got request: " + value); //Request-Line = Method SP Request-URI SP HTTP-Version CRLF int pos = value.IndexOf(' '); if (pos == -1 || pos + 1 >= value.Length) { m_log.Write(this, LogPrio.Warning, "Invalid request line, missing Method. Line: " + value); throw new BadRequestException("Invalid request line, missing Method. Line: " + value); } string method = value.Substring(0, pos).ToUpper(); int oldPos = pos + 1; pos = value.IndexOf(' ', oldPos); if (pos == -1) { m_log.Write(this, LogPrio.Warning, "Invalid request line, missing URI. Line: " + value); throw new BadRequestException("Invalid request line, missing URI. Line: " + value); } string path = value.Substring(oldPos, pos - oldPos); if (path.Length > 4196) throw new BadRequestException("Too long URI."); if (pos + 1 >= value.Length) { m_log.Write(this, LogPrio.Warning, "Invalid request line, missing HTTP-Version. Line: " + value); throw new BadRequestException("Invalid request line, missing HTTP-Version. Line: " + value); } string version = value.Substring(pos + 1); if (version.Length < 4 || string.Compare(version.Substring(0, 4), "HTTP", true) != 0) { m_log.Write(this, LogPrio.Warning, "Invalid HTTP version in request line. Line: " + value); throw new BadRequestException("Invalid HTTP version in Request line. Line: " + value); } m_requestLineArgs.HttpMethod = method; m_requestLineArgs.HttpVersion = version; m_requestLineArgs.UriPath = path; RequestLineReceived(this, m_requestLineArgs); } /// /// We've parsed a new header. /// /// Name in lower case /// Value, unmodified. /// If content length cannot be parsed. protected void OnHeader(string name, string value) { m_headerArgs.Name = name; m_headerArgs.Value = value; if (string.Compare(name, "content-length", true) == 0) { if (!int.TryParse(value, out m_bodyBytesLeft)) throw new BadRequestException("Content length is not a number."); } HeaderReceived?.Invoke(this, m_headerArgs); } private void OnRequestCompleted() { RequestCompleted?.Invoke(this, EventArgs.Empty); } #region IHttpRequestParser Members /// /// Current state in parser. /// public RequestParserState CurrentState { get; private set; } /// /// Parse a message /// /// bytes to parse. /// where in buffer that parsing should start /// number of bytes to parse, starting on . /// offset (where to start parsing next). /// BadRequestException. public int Parse(byte[] buffer, int offset, int count) { // add body bytes if (CurrentState == RequestParserState.Body) { return AddToBody(buffer, offset, count); } #if DEBUG string temp = Encoding.ASCII.GetString(buffer, offset, count); _log.Write(this, LogPrio.Trace, "\r\n\r\n HTTP MESSAGE: " + temp + "\r\n"); #endif int currentLine = 1; int startPos = -1; // set start pos since this is from an partial request if (CurrentState == RequestParserState.HeaderValue) startPos = 0; int endOfBufferPos = offset + count; // // Handled bytes are used to keep track of the number of bytes processed. // We do this since we can handle partial requests (to be able to check headers and abort // invalid requests directly without having to process the whole header / body). // int handledBytes = 0; for (int currentPos = offset; currentPos < endOfBufferPos; ++currentPos) { var ch = (char)buffer[currentPos]; char nextCh = endOfBufferPos > currentPos + 1 ? (char)buffer[currentPos + 1] : char.MinValue; if (ch == '\r') ++currentLine; switch (CurrentState) { case RequestParserState.FirstLine: if (currentPos == 8191) { m_log.Write(this, LogPrio.Warning, "HTTP Request is too large."); throw new BadRequestException("Too large request line."); } if (char.IsLetterOrDigit(ch) && startPos == -1) startPos = currentPos; if (startPos == -1 && (ch != '\r' || nextCh != '\n')) { m_log.Write(this, LogPrio.Warning, "Request line is not found."); throw new BadRequestException("Invalid request line."); } if (startPos != -1 && (ch == '\r' || ch == '\n')) { int size = GetLineBreakSize(buffer, currentPos); OnFirstLine(Encoding.UTF8.GetString(buffer, startPos, currentPos - startPos)); CurrentState = CurrentState + 1; currentPos += size - 1; handledBytes = currentPos + size - 1; startPos = -1; } break; case RequestParserState.HeaderName: if (ch == '\r' || ch == '\n') { currentPos += GetLineBreakSize(buffer, currentPos); if (m_bodyBytesLeft == 0) { CurrentState = RequestParserState.FirstLine; m_log.Write(this, LogPrio.Trace, "Request parsed successfully (no content)."); OnRequestCompleted(); Clear(); return currentPos; } CurrentState = RequestParserState.Body; if (currentPos + 1 < endOfBufferPos) { m_log.Write(this, LogPrio.Trace, "Adding bytes to the body"); return AddToBody(buffer, currentPos, endOfBufferPos - currentPos); } return currentPos; } if (char.IsWhiteSpace(ch) || ch == ':') { if (startPos == -1) { m_log.Write(this, LogPrio.Warning, "Expected header name, got colon on line " + currentLine); throw new BadRequestException("Expected header name, got colon on line " + currentLine); } m_curHeaderName = Encoding.UTF8.GetString(buffer, startPos, currentPos - startPos); handledBytes = currentPos + 1; startPos = -1; CurrentState = CurrentState + 1; if (ch == ':') CurrentState = CurrentState + 1; } else if (startPos == -1) startPos = currentPos; else if (!char.IsLetterOrDigit(ch) && ch != '-') { m_log.Write(this, LogPrio.Warning, "Invalid character in header name on line " + currentLine); throw new BadRequestException("Invalid character in header name on line " + currentLine); } if (startPos != -1 && currentPos - startPos > 200) { m_log.Write(this, LogPrio.Warning, "Invalid header name on line " + currentLine); throw new BadRequestException("Invalid header name on line " + currentLine); } break; case RequestParserState.AfterName: if (ch == ':') { handledBytes = currentPos + 1; CurrentState = CurrentState + 1; } break; case RequestParserState.Between: { if (ch == ' ' || ch == '\t') continue; int newLineSize = GetLineBreakSize(buffer, currentPos); if (newLineSize > 0 && currentPos + newLineSize < endOfBufferPos && char.IsWhiteSpace((char)buffer[currentPos + newLineSize])) { ++currentPos; continue; } startPos = currentPos; CurrentState = CurrentState + 1; handledBytes = currentPos; continue; } case RequestParserState.HeaderValue: { if (ch != '\r' && ch != '\n') continue; int newLineSize = GetLineBreakSize(buffer, currentPos); if (startPos == -1) continue; // allow new lines before start of value if (m_curHeaderName == string.Empty) throw new BadRequestException("Missing header on line " + currentLine); if (startPos == -1) { m_log.Write(this, LogPrio.Warning, "Missing header value for '" + m_curHeaderName); throw new BadRequestException("Missing header value for '" + m_curHeaderName); } if (currentPos - startPos > 8190) { m_log.Write(this, LogPrio.Warning, "Too large header value on line " + currentLine); throw new BadRequestException("Too large header value on line " + currentLine); } // Header fields can be extended over multiple lines by preceding each extra line with at // least one SP or HT. if (endOfBufferPos > currentPos + newLineSize && (buffer[currentPos + newLineSize] == ' ' || buffer[currentPos + newLineSize] == '\t')) { if (startPos != -1) m_curHeaderValue = Encoding.UTF8.GetString(buffer, startPos, currentPos - startPos); m_log.Write(this, LogPrio.Trace, "Header value is on multiple lines."); CurrentState = RequestParserState.Between; startPos = -1; currentPos += newLineSize - 1; handledBytes = currentPos + newLineSize - 1; continue; } m_curHeaderValue += Encoding.UTF8.GetString(buffer, startPos, currentPos - startPos); m_log.Write(this, LogPrio.Trace, "Header [" + m_curHeaderName + ": " + m_curHeaderValue + "]"); OnHeader(m_curHeaderName, m_curHeaderValue); startPos = -1; CurrentState = RequestParserState.HeaderName; m_curHeaderValue = string.Empty; m_curHeaderName = string.Empty; ++currentPos; handledBytes = currentPos + 1; // Check if we got a colon so we can cut header name, or crlf for end of header. bool canContinue = false; for (int j = currentPos; j < endOfBufferPos; ++j) { if (buffer[j] != ':' && buffer[j] != '\r' && buffer[j] != '\n') continue; canContinue = true; break; } if (!canContinue) { m_log.Write(this, LogPrio.Trace, "Cant continue, no colon."); return currentPos + 1; } } break; } } return handledBytes; } int GetLineBreakSize(byte[] buffer, int offset) { if (buffer[offset] == '\r') { if (buffer.Length > offset + 1 && buffer[offset + 1] == '\n') return 2; else throw new BadRequestException("Got invalid linefeed."); } else if (buffer[offset] == '\n') { if (buffer.Length == offset + 1) return 1; // linux line feed if (buffer[offset + 1] != '\r') return 1; // linux line feed else return 2; // win line feed } else return 0; } /// /// A request have been successfully parsed. /// public event EventHandler RequestCompleted; #endregion } }