123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415 |
- using System;
- using System.Text;
- using OSHttpServer.Exceptions;
- namespace OSHttpServer.Parser
- {
- /// <summary>
- /// Parses a HTTP request directly from a stream
- /// </summary>
- 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;
- /// <summary>
- /// More body bytes have been received.
- /// </summary>
- public event EventHandler<BodyEventArgs> BodyBytesReceived;
- /// <summary>
- /// Request line have been received.
- /// </summary>
- public event EventHandler<RequestLineEventArgs> RequestLineReceived;
- /// <summary>
- /// A header have been received.
- /// </summary>
- public event EventHandler<HeaderEventArgs> HeaderReceived;
- /// <summary>
- /// Create a new request parser
- /// </summary>
- /// <param name="logWriter">delegate receiving log entries.</param>
- public HttpRequestParser(ILogWriter logWriter)
- {
- m_log = logWriter ?? NullLogWriter.Instance;
- }
- /// <summary>
- /// Add a number of bytes to the body
- /// </summary>
- /// <param name="buffer">buffer containing more body bytes.</param>
- /// <param name="offset">starting offset in buffer</param>
- /// <param name="count">number of bytes, from offset, to read.</param>
- /// <returns>offset to continue from.</returns>
- 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;
- }
- /// <summary>
- /// Remove all state information for the request.
- /// </summary>
- public void Clear()
- {
- m_bodyBytesLeft = 0;
- m_curHeaderName = string.Empty;
- m_curHeaderValue = string.Empty;
- CurrentState = RequestParserState.FirstLine;
- }
- /// <summary>
- /// Gets or sets the log writer.
- /// </summary>
- public ILogWriter LogWriter
- {
- get { return m_log; }
- set { m_log = value ?? NullLogWriter.Instance; }
- }
- /// <summary>
- /// Parse request line
- /// </summary>
- /// <param name="value"></param>
- /// <exception cref="BadRequestException">If line is incorrect</exception>
- /// <remarks>Expects the following format: "Method SP Request-URI SP HTTP-Version CRLF"</remarks>
- 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 (path == "*")
- throw new BadRequestException("Not supported 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);
- }
- /// <summary>
- /// We've parsed a new header.
- /// </summary>
- /// <param name="name">Name in lower case</param>
- /// <param name="value">Value, unmodified.</param>
- /// <exception cref="BadRequestException">If content length cannot be parsed.</exception>
- 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
- /// <summary>
- /// Current state in parser.
- /// </summary>
- public RequestParserState CurrentState { get; private set; }
- /// <summary>
- /// Parse a message
- /// </summary>
- /// <param name="buffer">bytes to parse.</param>
- /// <param name="offset">where in buffer that parsing should start</param>
- /// <param name="count">number of bytes to parse, starting on <paramref name="offset"/>.</param>
- /// <returns>offset (where to start parsing next).</returns>
- /// <exception cref="BadRequestException"><c>BadRequestException</c>.</exception>
- 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;
- //<summary>
- // 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).
- // </summary>
- 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;
- }
- /// <summary>
- /// A request have been successfully parsed.
- /// </summary>
- public event EventHandler RequestCompleted;
- #endregion
- }
- }
|