1
0

HttpRequestParser.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. using System;
  2. using OSHttpServer.Exceptions;
  3. using OpenMetaverse;
  4. namespace OSHttpServer.Parser
  5. {
  6. /// <summary>
  7. /// Parses a HTTP request directly from a stream
  8. /// </summary>
  9. public class HttpRequestParser : IHttpRequestParser
  10. {
  11. private ILogWriter m_log;
  12. private readonly HeaderEventArgs m_headerArgs = new();
  13. private readonly BodyEventArgs m_bodyEventArgs = new();
  14. private readonly RequestLineEventArgs m_requestLineArgs = new();
  15. private osUTF8Slice m_curHeaderName = new();
  16. private osUTF8Slice m_curHeaderValue = new();
  17. private int m_bodyBytesLeft;
  18. /// <summary>
  19. /// More body bytes have been received.
  20. /// </summary>
  21. public event EventHandler<BodyEventArgs> BodyBytesReceived;
  22. /// <summary>
  23. /// Request line have been received.
  24. /// </summary>
  25. public event EventHandler<RequestLineEventArgs> RequestLineReceived;
  26. /// <summary>
  27. /// A header have been received.
  28. /// </summary>
  29. public event EventHandler<HeaderEventArgs> HeaderReceived;
  30. /// <summary>
  31. /// Create a new request parser
  32. /// </summary>
  33. /// <param name="logWriter">delegate receiving log entries.</param>
  34. public HttpRequestParser(ILogWriter logWriter)
  35. {
  36. m_log = logWriter ?? NullLogWriter.Instance;
  37. }
  38. /// <summary>
  39. /// Add a number of bytes to the body
  40. /// </summary>
  41. /// <param name="buffer">buffer containing more body bytes.</param>
  42. /// <param name="offset">starting offset in buffer</param>
  43. /// <param name="count">number of bytes, from offset, to read.</param>
  44. /// <returns>offset to continue from.</returns>
  45. private int AddToBody(byte[] buffer, int offset, int count)
  46. {
  47. // got all bytes we need, or just a few of them?
  48. int bytesCount = count > m_bodyBytesLeft ? m_bodyBytesLeft : count;
  49. if(BodyBytesReceived != null)
  50. {
  51. m_bodyEventArgs.Buffer = buffer;
  52. m_bodyEventArgs.Offset = offset;
  53. m_bodyEventArgs.Count = bytesCount;
  54. BodyBytesReceived?.Invoke(this, m_bodyEventArgs);
  55. m_bodyEventArgs.Buffer = null;
  56. }
  57. m_bodyBytesLeft -= bytesCount;
  58. if (m_bodyBytesLeft == 0)
  59. {
  60. // got a complete request.
  61. m_log.Write(this, LogPrio.Trace, "Request parsed successfully");
  62. OnRequestCompleted();
  63. Clear();
  64. }
  65. return offset + bytesCount;
  66. }
  67. /// <summary>
  68. /// Remove all state information for the request.
  69. /// </summary>
  70. public void Clear()
  71. {
  72. m_bodyBytesLeft = 0;
  73. m_curHeaderName.Clear();
  74. m_curHeaderValue.Clear();
  75. CurrentState = RequestParserState.FirstLine;
  76. }
  77. /// <summary>
  78. /// Gets or sets the log writer.
  79. /// </summary>
  80. public ILogWriter LogWriter
  81. {
  82. get { return m_log; }
  83. set { m_log = value ?? NullLogWriter.Instance; }
  84. }
  85. /// <summary>
  86. /// Parse request line
  87. /// </summary>
  88. /// <param name="value"></param>
  89. /// <exception cref="BadRequestException">If line is incorrect</exception>
  90. /// <remarks>Expects the following format: "Method SP Request-URI SP HTTP-Version CRLF"</remarks>
  91. protected void OnFirstLine(osUTF8Slice value)
  92. {
  93. //
  94. //todo: In the interest of robustness, servers SHOULD ignore any empty line(s) received where a Request-Line is expected.
  95. // 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.
  96. //
  97. m_log.Write(this, LogPrio.Debug, $"Got request: {value}");
  98. //Request-Line = Method SP Request-URI SP HTTP-Version CRLF
  99. int pos = value.IndexOf((byte)' ');
  100. int oldPos = pos + 1;
  101. if (pos == -1 || oldPos >= value.Length)
  102. {
  103. m_log.Write(this, LogPrio.Warning, $"Invalid request line, missing Method. Line: {value}");
  104. throw new BadRequestException($"Invalid request line, missing Method. Line: {value}");
  105. }
  106. osUTF8Slice method = value.SubUTF8(0, pos);
  107. value.SubUTF8Self(oldPos);
  108. pos = value.IndexOf((byte)' ');
  109. if (pos == -1)
  110. {
  111. m_log.Write(this, LogPrio.Warning, "Invalid request line, missing URI");
  112. throw new BadRequestException("Invalid request line, missing URI");
  113. }
  114. if(pos > 4196)
  115. throw new BadRequestException("URI too long");
  116. osUTF8Slice path = value.SubUTF8(0, pos);
  117. if (path.ACSIILowerEquals("*"))
  118. throw new BadRequestException("URI not supported");
  119. oldPos = pos + 1;
  120. if (oldPos >= value.Length)
  121. {
  122. m_log.Write(this, LogPrio.Warning, $"Invalid request line, missing HTTP-Version. Line: {value}");
  123. throw new BadRequestException($"Invalid request line, missing HTTP-Version. Line: {value}");
  124. }
  125. osUTF8Slice version = value.SubUTF8(oldPos);
  126. if (version.Length < 4 || !version.SubUTF8(0,4).ACSIILowerEquals("http"))
  127. {
  128. m_log.Write(this, LogPrio.Warning, $"Invalid HTTP version in request line. Line: {value}");
  129. throw new BadRequestException($"Invalid HTTP version in Request line. Line: {value}");
  130. }
  131. if(RequestLineReceived is not null)
  132. {
  133. method.ToASCIIUpperSelf();
  134. m_requestLineArgs.HttpMethod = method;
  135. m_requestLineArgs.HttpVersion = version;
  136. m_requestLineArgs.UriPath = path;
  137. RequestLineReceived?.Invoke(this, m_requestLineArgs);
  138. }
  139. }
  140. private static readonly byte[] OSUTF8contentlength = osUTF8.GetASCIIBytes("content-length");
  141. /// <summary>
  142. /// We've parsed a new header.
  143. /// </summary>
  144. /// <param name="name">Name in lower case</param>
  145. /// <param name="value">Value, unmodified.</param>
  146. /// <exception cref="BadRequestException">If content length cannot be parsed.</exception>
  147. protected void OnHeader()
  148. {
  149. if (m_curHeaderName.ACSIILowerEquals(OSUTF8contentlength))
  150. {
  151. if (!m_curHeaderValue.TryParseInt(out m_bodyBytesLeft))
  152. throw new BadRequestException("Content length is not a number.");
  153. if(m_bodyBytesLeft > 64 * 1024 * 1204)
  154. throw new BadRequestException("Content length too large.");
  155. }
  156. if (HeaderReceived != null)
  157. {
  158. m_headerArgs.Name = m_curHeaderName;
  159. m_headerArgs.Value = m_curHeaderValue;
  160. HeaderReceived?.Invoke(this, m_headerArgs);
  161. }
  162. m_curHeaderName.Clear();
  163. m_curHeaderValue.Clear();
  164. }
  165. private void OnRequestCompleted()
  166. {
  167. RequestCompleted?.Invoke(this, EventArgs.Empty);
  168. }
  169. #region IHttpRequestParser Members
  170. /// <summary>
  171. /// Current state in parser.
  172. /// </summary>
  173. public RequestParserState CurrentState { get; private set; }
  174. /// <summary>
  175. /// Parse a message
  176. /// </summary>
  177. /// <param name="buffer">bytes to parse.</param>
  178. /// <param name="offset">where in buffer that parsing should start</param>
  179. /// <param name="count">number of bytes to parse, starting on <paramref name="offset"/>.</param>
  180. /// <returns>offset (where to start parsing next).</returns>
  181. /// <exception cref="BadRequestException"><c>BadRequestException</c>.</exception>
  182. public int Parse(byte[] buffer, int offset, int count)
  183. {
  184. // add body bytes
  185. if (CurrentState == RequestParserState.Body)
  186. {
  187. return AddToBody(buffer, offset, count);
  188. }
  189. int currentLine = 1;
  190. int startPos = -1;
  191. // set start pos since this is from an partial request
  192. if (CurrentState == RequestParserState.HeaderValue)
  193. startPos = 0;
  194. int endOfBufferPos = offset + count;
  195. //<summary>
  196. // Handled bytes are used to keep track of the number of bytes processed.
  197. // We do this since we can handle partial requests (to be able to check headers and abort
  198. // invalid requests directly without having to process the whole header / body).
  199. // </summary>
  200. int handledBytes = 0;
  201. for (int currentPos = offset; currentPos < endOfBufferPos; ++currentPos)
  202. {
  203. var ch = (char)buffer[currentPos];
  204. char nextCh = endOfBufferPos > currentPos + 1 ? (char)buffer[currentPos + 1] : char.MinValue;
  205. if (ch == '\r')
  206. ++currentLine;
  207. switch (CurrentState)
  208. {
  209. case RequestParserState.FirstLine:
  210. if (currentPos == 8191)
  211. {
  212. m_log.Write(this, LogPrio.Warning, "HTTP Request is too large.");
  213. throw new BadRequestException("Too large request line.");
  214. }
  215. if (startPos == -1)
  216. {
  217. if(char.IsLetterOrDigit(ch))
  218. startPos = currentPos;
  219. else if (ch != '\r' || nextCh != '\n')
  220. {
  221. m_log.Write(this, LogPrio.Warning, "Request line is not found.");
  222. throw new BadRequestException("Invalid request line.");
  223. }
  224. }
  225. else if(ch == '\r' || ch == '\n')
  226. {
  227. int size = GetLineBreakSize(buffer, currentPos);
  228. OnFirstLine(new osUTF8Slice(buffer, startPos, currentPos - startPos));
  229. currentPos += size - 1;
  230. handledBytes = currentPos + 1;
  231. startPos = -1;
  232. CurrentState = RequestParserState.HeaderName;
  233. }
  234. break;
  235. case RequestParserState.HeaderName:
  236. if (ch == '\r' || ch == '\n')
  237. {
  238. currentPos += GetLineBreakSize(buffer, currentPos);
  239. if (m_bodyBytesLeft == 0)
  240. {
  241. CurrentState = RequestParserState.FirstLine;
  242. m_log.Write(this, LogPrio.Trace, "Request parsed successfully (no content)");
  243. OnRequestCompleted();
  244. Clear();
  245. return currentPos;
  246. }
  247. CurrentState = RequestParserState.Body;
  248. if (currentPos + 1 < endOfBufferPos)
  249. {
  250. m_log.Write(this, LogPrio.Trace, "Adding bytes to the body");
  251. return AddToBody(buffer, currentPos, endOfBufferPos - currentPos);
  252. }
  253. return currentPos;
  254. }
  255. if (char.IsWhiteSpace(ch) || ch == ':')
  256. {
  257. if (startPos == -1)
  258. {
  259. m_log.Write(this, LogPrio.Warning, $"Expected header name, got colon on line {currentLine}");
  260. throw new BadRequestException($"Expected header name, got colon on line {currentLine}");
  261. }
  262. m_curHeaderName = new osUTF8Slice(buffer, startPos, currentPos - startPos);
  263. handledBytes = currentPos + 1;
  264. startPos = handledBytes;
  265. if (ch == ':')
  266. CurrentState = RequestParserState.Between;
  267. else
  268. CurrentState = RequestParserState.AfterName;
  269. }
  270. else if (!char.IsLetterOrDigit(ch) && ch != '-')
  271. {
  272. m_log.Write(this, LogPrio.Warning, $"Invalid character in header name on line {currentLine}");
  273. throw new BadRequestException($"Invalid character in header name on line {currentLine}");
  274. }
  275. if (startPos == -1)
  276. startPos = currentPos;
  277. else if (currentPos - startPos > 200)
  278. {
  279. m_log.Write(this, LogPrio.Warning, $"Invalid header name on line {currentLine}");
  280. throw new BadRequestException($"Invalid header name on line {currentLine}");
  281. }
  282. break;
  283. case RequestParserState.AfterName:
  284. if (ch == ':')
  285. {
  286. handledBytes = currentPos + 1;
  287. startPos = currentPos;
  288. CurrentState = RequestParserState.Between;
  289. }
  290. else if(currentPos - startPos > 256)
  291. {
  292. m_log.Write(this, LogPrio.Warning, "missing header aftername ':' " + currentLine);
  293. throw new BadRequestException("missing header aftername ':' " + currentLine);
  294. }
  295. break;
  296. case RequestParserState.Between:
  297. {
  298. if (ch == ' ' || ch == '\t')
  299. {
  300. if (currentPos - startPos > 256)
  301. {
  302. m_log.Write(this, LogPrio.Warning, $"header value too far {currentLine}");
  303. throw new BadRequestException($"header value too far {currentLine}");
  304. }
  305. }
  306. else
  307. {
  308. int newLineSize = GetLineBreakSize(buffer, currentPos);
  309. int tsize = currentPos + newLineSize;
  310. if (newLineSize > 0 && tsize < endOfBufferPos &&
  311. char.IsWhiteSpace((char)buffer[tsize]))
  312. {
  313. if (currentPos - startPos > 256)
  314. {
  315. m_log.Write(this, LogPrio.Warning, $"header value too far {currentLine}");
  316. throw new BadRequestException($"header value too far {currentLine}");
  317. }
  318. ++currentPos;
  319. }
  320. else
  321. {
  322. startPos = currentPos;
  323. handledBytes = currentPos;
  324. CurrentState = RequestParserState.HeaderValue;
  325. }
  326. }
  327. break;
  328. }
  329. case RequestParserState.HeaderValue:
  330. {
  331. if (ch == '\r' || ch == '\n')
  332. {
  333. if (m_curHeaderName.Length == 0)
  334. throw new BadRequestException($"Missing header on line {currentLine}");
  335. if (currentPos - startPos > 8190)
  336. {
  337. m_log.Write(this, LogPrio.Warning, $"Too large header value on line {currentLine}");
  338. throw new BadRequestException($"Too large header value on line {currentLine}");
  339. }
  340. // Header fields can be extended over multiple lines by preceding each extra line with at
  341. // least one SP or HT.
  342. int newLineSize = GetLineBreakSize(buffer, currentPos);
  343. int tnext = currentPos + newLineSize;
  344. if (endOfBufferPos > tnext && (buffer[tnext] == ' ' || buffer[tnext] == '\t'))
  345. {
  346. if (startPos != -1)
  347. {
  348. osUTF8Slice osUTF8SliceTmp = new(buffer, startPos, currentPos - startPos);
  349. if (m_curHeaderValue.Length == 0)
  350. m_curHeaderValue = osUTF8SliceTmp.Clone();
  351. else
  352. m_curHeaderValue.Append(osUTF8SliceTmp);
  353. }
  354. m_log.Write(this, LogPrio.Trace, "Header value is on multiple lines.");
  355. CurrentState = RequestParserState.Between;
  356. currentPos += newLineSize - 1;
  357. startPos = currentPos;
  358. handledBytes = currentPos + 1;
  359. }
  360. else
  361. {
  362. osUTF8Slice osUTF8SliceTmp = new(buffer, startPos, currentPos - startPos);
  363. if (m_curHeaderValue.Length == 0)
  364. m_curHeaderValue = osUTF8SliceTmp.Clone();
  365. else
  366. m_curHeaderValue.Append(osUTF8SliceTmp);
  367. m_log.Write(this, LogPrio.Trace, $"Header [{m_curHeaderName}:{m_curHeaderValue}]");
  368. OnHeader();
  369. startPos = -1;
  370. CurrentState = RequestParserState.HeaderName;
  371. currentPos += newLineSize - 1;
  372. handledBytes = currentPos + 1;
  373. // Check if we got a colon so we can cut header name, or crlf for end of header.
  374. bool canContinue = false;
  375. for (int j = currentPos; j < endOfBufferPos; ++j)
  376. {
  377. if (buffer[j] != ':' && buffer[j] != '\r' && buffer[j] != '\n')
  378. continue;
  379. canContinue = true;
  380. break;
  381. }
  382. if (!canContinue)
  383. {
  384. m_log.Write(this, LogPrio.Trace, "Cant continue, no colon.");
  385. return currentPos + 1;
  386. }
  387. }
  388. }
  389. }
  390. break;
  391. }
  392. }
  393. return handledBytes;
  394. }
  395. static int GetLineBreakSize(byte[] buffer, int offset)
  396. {
  397. byte c = buffer[offset];
  398. if (c == '\r')
  399. {
  400. ++offset;
  401. if (buffer.Length > offset && buffer[offset] == '\n')
  402. return 2;
  403. else
  404. throw new BadRequestException("Got invalid linefeed.");
  405. }
  406. else if (c == '\n')
  407. {
  408. ++offset;
  409. if (buffer.Length == offset)
  410. return 1; // linux line feed
  411. return buffer[offset] == '\r' ? 2 : 1;
  412. }
  413. else
  414. return 0;
  415. }
  416. /// <summary>
  417. /// A request have been successfully parsed.
  418. /// </summary>
  419. public event EventHandler RequestCompleted;
  420. #endregion
  421. }
  422. }