using System; using OSHttpServer.Exceptions; using OpenMetaverse; namespace OSHttpServer.Parser { /// /// Parses a HTTP request directly from a stream /// public class HttpRequestParser : IHttpRequestParser { private ILogWriter m_log; private readonly HeaderEventArgs m_headerArgs = new(); private readonly BodyEventArgs m_bodyEventArgs = new(); private readonly RequestLineEventArgs m_requestLineArgs = new(); private osUTF8Slice m_curHeaderName = new(); private osUTF8Slice m_curHeaderValue = new(); 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 bytesCount = count > m_bodyBytesLeft ? m_bodyBytesLeft : count; if(BodyBytesReceived != null) { m_bodyEventArgs.Buffer = buffer; m_bodyEventArgs.Offset = offset; m_bodyEventArgs.Count = bytesCount; BodyBytesReceived?.Invoke(this, m_bodyEventArgs); m_bodyEventArgs.Buffer = null; } m_bodyBytesLeft -= bytesCount; if (m_bodyBytesLeft == 0) { // got a complete request. m_log.Write(this, LogPrio.Trace, "Request parsed successfully"); OnRequestCompleted(); Clear(); } return offset + bytesCount; } /// /// Remove all state information for the request. /// public void Clear() { m_bodyBytesLeft = 0; m_curHeaderName.Clear(); m_curHeaderValue.Clear(); 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(osUTF8Slice 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((byte)' '); int oldPos = pos + 1; if (pos == -1 || oldPos >= 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}"); } osUTF8Slice method = value.SubUTF8(0, pos); value.SubUTF8Self(oldPos); pos = value.IndexOf((byte)' '); if (pos == -1) { m_log.Write(this, LogPrio.Warning, "Invalid request line, missing URI"); throw new BadRequestException("Invalid request line, missing URI"); } if(pos > 4196) throw new BadRequestException("URI too long"); osUTF8Slice path = value.SubUTF8(0, pos); if (path.ACSIILowerEquals("*")) throw new BadRequestException("URI not supported"); oldPos = pos + 1; if (oldPos >= 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}"); } osUTF8Slice version = value.SubUTF8(oldPos); if (version.Length < 4 || !version.SubUTF8(0,4).ACSIILowerEquals("http")) { 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}"); } if(RequestLineReceived is not null) { method.ToASCIIUpperSelf(); m_requestLineArgs.HttpMethod = method; m_requestLineArgs.HttpVersion = version; m_requestLineArgs.UriPath = path; RequestLineReceived?.Invoke(this, m_requestLineArgs); } } private static readonly byte[] OSUTF8contentlength = osUTF8.GetASCIIBytes("content-length"); /// /// We've parsed a new header. /// /// Name in lower case /// Value, unmodified. /// If content length cannot be parsed. protected void OnHeader() { if (m_curHeaderName.ACSIILowerEquals(OSUTF8contentlength)) { if (!m_curHeaderValue.TryParseInt(out m_bodyBytesLeft)) throw new BadRequestException("Content length is not a number."); if(m_bodyBytesLeft > 64 * 1024 * 1204) throw new BadRequestException("Content length too large."); } if (HeaderReceived != null) { m_headerArgs.Name = m_curHeaderName; m_headerArgs.Value = m_curHeaderValue; HeaderReceived?.Invoke(this, m_headerArgs); } m_curHeaderName.Clear(); m_curHeaderValue.Clear(); } 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); } 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 (startPos == -1) { if(char.IsLetterOrDigit(ch)) startPos = currentPos; else if (ch != '\r' || nextCh != '\n') { m_log.Write(this, LogPrio.Warning, "Request line is not found."); throw new BadRequestException("Invalid request line."); } } else if(ch == '\r' || ch == '\n') { int size = GetLineBreakSize(buffer, currentPos); OnFirstLine(new osUTF8Slice(buffer, startPos, currentPos - startPos)); currentPos += size - 1; handledBytes = currentPos + 1; startPos = -1; CurrentState = RequestParserState.HeaderName; } 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 = new osUTF8Slice(buffer, startPos, currentPos - startPos); handledBytes = currentPos + 1; startPos = handledBytes; if (ch == ':') CurrentState = RequestParserState.Between; else CurrentState = RequestParserState.AfterName; } 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) startPos = currentPos; else if (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; startPos = currentPos; CurrentState = RequestParserState.Between; } else if(currentPos - startPos > 256) { m_log.Write(this, LogPrio.Warning, "missing header aftername ':' " + currentLine); throw new BadRequestException("missing header aftername ':' " + currentLine); } break; case RequestParserState.Between: { if (ch == ' ' || ch == '\t') { if (currentPos - startPos > 256) { m_log.Write(this, LogPrio.Warning, $"header value too far {currentLine}"); throw new BadRequestException($"header value too far {currentLine}"); } } else { int newLineSize = GetLineBreakSize(buffer, currentPos); int tsize = currentPos + newLineSize; if (newLineSize > 0 && tsize < endOfBufferPos && char.IsWhiteSpace((char)buffer[tsize])) { if (currentPos - startPos > 256) { m_log.Write(this, LogPrio.Warning, $"header value too far {currentLine}"); throw new BadRequestException($"header value too far {currentLine}"); } ++currentPos; } else { startPos = currentPos; handledBytes = currentPos; CurrentState = RequestParserState.HeaderValue; } } break; } case RequestParserState.HeaderValue: { if (ch == '\r' || ch == '\n') { if (m_curHeaderName.Length == 0) throw new BadRequestException($"Missing header on line {currentLine}"); 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. int newLineSize = GetLineBreakSize(buffer, currentPos); int tnext = currentPos + newLineSize; if (endOfBufferPos > tnext && (buffer[tnext] == ' ' || buffer[tnext] == '\t')) { if (startPos != -1) { osUTF8Slice osUTF8SliceTmp = new(buffer, startPos, currentPos - startPos); if (m_curHeaderValue.Length == 0) m_curHeaderValue = osUTF8SliceTmp.Clone(); else m_curHeaderValue.Append(osUTF8SliceTmp); } m_log.Write(this, LogPrio.Trace, "Header value is on multiple lines."); CurrentState = RequestParserState.Between; currentPos += newLineSize - 1; startPos = currentPos; handledBytes = currentPos + 1; } else { osUTF8Slice osUTF8SliceTmp = new(buffer, startPos, currentPos - startPos); if (m_curHeaderValue.Length == 0) m_curHeaderValue = osUTF8SliceTmp.Clone(); else m_curHeaderValue.Append(osUTF8SliceTmp); m_log.Write(this, LogPrio.Trace, $"Header [{m_curHeaderName}:{m_curHeaderValue}]"); OnHeader(); startPos = -1; CurrentState = RequestParserState.HeaderName; currentPos += newLineSize - 1; 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; } static int GetLineBreakSize(byte[] buffer, int offset) { byte c = buffer[offset]; if (c == '\r') { ++offset; if (buffer.Length > offset && buffer[offset] == '\n') return 2; else throw new BadRequestException("Got invalid linefeed."); } else if (c == '\n') { ++offset; if (buffer.Length == offset) return 1; // linux line feed return buffer[offset] == '\r' ? 2 : 1; } else return 0; } /// /// A request have been successfully parsed. /// public event EventHandler RequestCompleted; #endregion } }