HttpListenerRequest.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  1. using System;
  2. using System.Collections.Specialized;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Net;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8. using MediaBrowser.Model.Net;
  9. using MediaBrowser.Model.Services;
  10. using MediaBrowser.Model.Text;
  11. using SocketHttpListener.Primitives;
  12. namespace SocketHttpListener.Net
  13. {
  14. public sealed class HttpListenerRequest
  15. {
  16. string[] accept_types;
  17. Encoding content_encoding;
  18. long content_length;
  19. bool cl_set;
  20. CookieCollection cookies;
  21. WebHeaderCollection headers;
  22. string method;
  23. Stream input_stream;
  24. Version version;
  25. QueryParamCollection query_string; // check if null is ok, check if read-only, check case-sensitiveness
  26. string raw_url;
  27. Uri url;
  28. Uri referrer;
  29. string[] user_languages;
  30. HttpListenerContext context;
  31. bool is_chunked;
  32. bool ka_set;
  33. bool keep_alive;
  34. private readonly ITextEncoding _textEncoding;
  35. internal HttpListenerRequest(HttpListenerContext context, ITextEncoding textEncoding)
  36. {
  37. this.context = context;
  38. _textEncoding = textEncoding;
  39. headers = new WebHeaderCollection();
  40. version = HttpVersion.Version10;
  41. }
  42. static char[] separators = new char[] { ' ' };
  43. internal void SetRequestLine(string req)
  44. {
  45. string[] parts = req.Split(separators, 3);
  46. if (parts.Length != 3)
  47. {
  48. context.ErrorMessage = "Invalid request line (parts).";
  49. return;
  50. }
  51. method = parts[0];
  52. foreach (char c in method)
  53. {
  54. int ic = (int)c;
  55. if ((ic >= 'A' && ic <= 'Z') ||
  56. (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' &&
  57. c != '<' && c != '>' && c != '@' && c != ',' && c != ';' &&
  58. c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' &&
  59. c != ']' && c != '?' && c != '=' && c != '{' && c != '}'))
  60. continue;
  61. context.ErrorMessage = "(Invalid verb)";
  62. return;
  63. }
  64. raw_url = parts[1];
  65. if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/"))
  66. {
  67. context.ErrorMessage = "Invalid request line (version).";
  68. return;
  69. }
  70. try
  71. {
  72. version = new Version(parts[2].Substring(5));
  73. if (version.Major < 1)
  74. throw new Exception();
  75. }
  76. catch
  77. {
  78. context.ErrorMessage = "Invalid request line (version).";
  79. return;
  80. }
  81. }
  82. void CreateQueryString(string query)
  83. {
  84. if (query == null || query.Length == 0)
  85. {
  86. query_string = new QueryParamCollection();
  87. return;
  88. }
  89. query_string = new QueryParamCollection();
  90. if (query[0] == '?')
  91. query = query.Substring(1);
  92. string[] components = query.Split('&');
  93. foreach (string kv in components)
  94. {
  95. int pos = kv.IndexOf('=');
  96. if (pos == -1)
  97. {
  98. query_string.Add(null, WebUtility.UrlDecode(kv));
  99. }
  100. else
  101. {
  102. string key = WebUtility.UrlDecode(kv.Substring(0, pos));
  103. string val = WebUtility.UrlDecode(kv.Substring(pos + 1));
  104. query_string.Add(key, val);
  105. }
  106. }
  107. }
  108. internal void FinishInitialization()
  109. {
  110. string host = UserHostName;
  111. if (version > HttpVersion.Version10 && (host == null || host.Length == 0))
  112. {
  113. context.ErrorMessage = "Invalid host name";
  114. return;
  115. }
  116. string path;
  117. Uri raw_uri = null;
  118. if (MaybeUri(raw_url.ToLowerInvariant()) && Uri.TryCreate(raw_url, UriKind.Absolute, out raw_uri))
  119. path = raw_uri.PathAndQuery;
  120. else
  121. path = raw_url;
  122. if ((host == null || host.Length == 0))
  123. host = UserHostAddress;
  124. if (raw_uri != null)
  125. host = raw_uri.Host;
  126. int colon = host.LastIndexOf(':');
  127. if (colon >= 0)
  128. host = host.Substring(0, colon);
  129. string base_uri = String.Format("{0}://{1}:{2}",
  130. (IsSecureConnection) ? (IsWebSocketRequest ? "wss" : "https") : (IsWebSocketRequest ? "ws" : "http"),
  131. host, LocalEndPoint.Port);
  132. if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out url))
  133. {
  134. context.ErrorMessage = WebUtility.HtmlEncode("Invalid url: " + base_uri + path);
  135. return; return;
  136. }
  137. CreateQueryString(url.Query);
  138. if (version >= HttpVersion.Version11)
  139. {
  140. string t_encoding = Headers["Transfer-Encoding"];
  141. is_chunked = (t_encoding != null && String.Compare(t_encoding, "chunked", StringComparison.OrdinalIgnoreCase) == 0);
  142. // 'identity' is not valid!
  143. if (t_encoding != null && !is_chunked)
  144. {
  145. context.Connection.SendError(null, 501);
  146. return;
  147. }
  148. }
  149. if (!is_chunked && !cl_set)
  150. {
  151. if (String.Compare(method, "POST", StringComparison.OrdinalIgnoreCase) == 0 ||
  152. String.Compare(method, "PUT", StringComparison.OrdinalIgnoreCase) == 0)
  153. {
  154. context.Connection.SendError(null, 411);
  155. return;
  156. }
  157. }
  158. if (String.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0)
  159. {
  160. var output = (HttpResponseStream)context.Connection.GetResponseStream(true);
  161. var _100continue = _textEncoding.GetASCIIEncoding().GetBytes("HTTP/1.1 100 Continue\r\n\r\n");
  162. output.InternalWrite(_100continue, 0, _100continue.Length);
  163. }
  164. }
  165. static bool MaybeUri(string s)
  166. {
  167. int p = s.IndexOf(':');
  168. if (p == -1)
  169. return false;
  170. if (p >= 10)
  171. return false;
  172. return IsPredefinedScheme(s.Substring(0, p));
  173. }
  174. //
  175. // Using a simple block of if's is twice as slow as the compiler generated
  176. // switch statement. But using this tuned code is faster than the
  177. // compiler generated code, with a million loops on x86-64:
  178. //
  179. // With "http": .10 vs .51 (first check)
  180. // with "https": .16 vs .51 (second check)
  181. // with "foo": .22 vs .31 (never found)
  182. // with "mailto": .12 vs .51 (last check)
  183. //
  184. //
  185. static bool IsPredefinedScheme(string scheme)
  186. {
  187. if (scheme == null || scheme.Length < 3)
  188. return false;
  189. char c = scheme[0];
  190. if (c == 'h')
  191. return (scheme == "http" || scheme == "https");
  192. if (c == 'f')
  193. return (scheme == "file" || scheme == "ftp");
  194. if (c == 'n')
  195. {
  196. c = scheme[1];
  197. if (c == 'e')
  198. return (scheme == "news" || scheme == "net.pipe" || scheme == "net.tcp");
  199. if (scheme == "nntp")
  200. return true;
  201. return false;
  202. }
  203. if ((c == 'g' && scheme == "gopher") || (c == 'm' && scheme == "mailto"))
  204. return true;
  205. return false;
  206. }
  207. internal static string Unquote(String str)
  208. {
  209. int start = str.IndexOf('\"');
  210. int end = str.LastIndexOf('\"');
  211. if (start >= 0 && end >= 0)
  212. str = str.Substring(start + 1, end - 1);
  213. return str.Trim();
  214. }
  215. internal void AddHeader(string header)
  216. {
  217. int colon = header.IndexOf(':');
  218. if (colon == -1 || colon == 0)
  219. {
  220. context.ErrorMessage = "Bad Request";
  221. context.ErrorStatus = 400;
  222. return;
  223. }
  224. string name = header.Substring(0, colon).Trim();
  225. string val = header.Substring(colon + 1).Trim();
  226. string lower = name.ToLowerInvariant();
  227. headers.SetInternal(name, val);
  228. switch (lower)
  229. {
  230. case "accept-language":
  231. user_languages = val.Split(','); // yes, only split with a ','
  232. break;
  233. case "accept":
  234. accept_types = val.Split(','); // yes, only split with a ','
  235. break;
  236. case "content-length":
  237. try
  238. {
  239. //TODO: max. content_length?
  240. content_length = Int64.Parse(val.Trim());
  241. if (content_length < 0)
  242. context.ErrorMessage = "Invalid Content-Length.";
  243. cl_set = true;
  244. }
  245. catch
  246. {
  247. context.ErrorMessage = "Invalid Content-Length.";
  248. }
  249. break;
  250. case "content-type":
  251. {
  252. var contents = val.Split(';');
  253. foreach (var content in contents)
  254. {
  255. var tmp = content.Trim();
  256. if (tmp.StartsWith("charset"))
  257. {
  258. var charset = tmp.GetValue("=");
  259. if (charset != null && charset.Length > 0)
  260. {
  261. try
  262. {
  263. // Support upnp/dlna devices - CONTENT-TYPE: text/xml ; charset="utf-8"\r\n
  264. charset = charset.Trim('"');
  265. var index = charset.IndexOf('"');
  266. if (index != -1) charset = charset.Substring(0, index);
  267. content_encoding = Encoding.GetEncoding(charset);
  268. }
  269. catch
  270. {
  271. context.ErrorMessage = "Invalid Content-Type header: " + charset;
  272. }
  273. }
  274. break;
  275. }
  276. }
  277. }
  278. break;
  279. case "referer":
  280. try
  281. {
  282. referrer = new Uri(val);
  283. }
  284. catch
  285. {
  286. referrer = new Uri("http://someone.is.screwing.with.the.headers.com/");
  287. }
  288. break;
  289. case "cookie":
  290. if (cookies == null)
  291. cookies = new CookieCollection();
  292. string[] cookieStrings = val.Split(new char[] { ',', ';' });
  293. Cookie current = null;
  294. int version = 0;
  295. foreach (string cookieString in cookieStrings)
  296. {
  297. string str = cookieString.Trim();
  298. if (str.Length == 0)
  299. continue;
  300. if (str.StartsWith("$Version"))
  301. {
  302. version = Int32.Parse(Unquote(str.Substring(str.IndexOf('=') + 1)));
  303. }
  304. else if (str.StartsWith("$Path"))
  305. {
  306. if (current != null)
  307. current.Path = str.Substring(str.IndexOf('=') + 1).Trim();
  308. }
  309. else if (str.StartsWith("$Domain"))
  310. {
  311. if (current != null)
  312. current.Domain = str.Substring(str.IndexOf('=') + 1).Trim();
  313. }
  314. else if (str.StartsWith("$Port"))
  315. {
  316. if (current != null)
  317. current.Port = str.Substring(str.IndexOf('=') + 1).Trim();
  318. }
  319. else
  320. {
  321. if (current != null)
  322. {
  323. cookies.Add(current);
  324. }
  325. current = new Cookie();
  326. int idx = str.IndexOf('=');
  327. if (idx > 0)
  328. {
  329. current.Name = str.Substring(0, idx).Trim();
  330. current.Value = str.Substring(idx + 1).Trim();
  331. }
  332. else
  333. {
  334. current.Name = str.Trim();
  335. current.Value = String.Empty;
  336. }
  337. current.Version = version;
  338. }
  339. }
  340. if (current != null)
  341. {
  342. cookies.Add(current);
  343. }
  344. break;
  345. }
  346. }
  347. // returns true is the stream could be reused.
  348. internal bool FlushInput()
  349. {
  350. if (!HasEntityBody)
  351. return true;
  352. int length = 2048;
  353. if (content_length > 0)
  354. length = (int)Math.Min(content_length, (long)length);
  355. byte[] bytes = new byte[length];
  356. while (true)
  357. {
  358. // TODO: test if MS has a timeout when doing this
  359. try
  360. {
  361. var task = InputStream.ReadAsync(bytes, 0, length);
  362. var result = Task.WaitAll(new [] { task }, 1000);
  363. if (!result)
  364. {
  365. return false;
  366. }
  367. if (task.Result <= 0)
  368. {
  369. return true;
  370. }
  371. }
  372. catch (ObjectDisposedException e)
  373. {
  374. input_stream = null;
  375. return true;
  376. }
  377. catch
  378. {
  379. return false;
  380. }
  381. }
  382. }
  383. public string[] AcceptTypes
  384. {
  385. get { return accept_types; }
  386. }
  387. public int ClientCertificateError
  388. {
  389. get
  390. {
  391. HttpConnection cnc = context.Connection;
  392. //if (cnc.ClientCertificate == null)
  393. // throw new InvalidOperationException("No client certificate");
  394. //int[] errors = cnc.ClientCertificateErrors;
  395. //if (errors != null && errors.Length > 0)
  396. // return errors[0];
  397. return 0;
  398. }
  399. }
  400. public Encoding ContentEncoding
  401. {
  402. get
  403. {
  404. if (content_encoding == null)
  405. content_encoding = _textEncoding.GetDefaultEncoding();
  406. return content_encoding;
  407. }
  408. }
  409. public long ContentLength64
  410. {
  411. get { return is_chunked ? -1 : content_length; }
  412. }
  413. public string ContentType
  414. {
  415. get { return headers["content-type"]; }
  416. }
  417. public CookieCollection Cookies
  418. {
  419. get
  420. {
  421. // TODO: check if the collection is read-only
  422. if (cookies == null)
  423. cookies = new CookieCollection();
  424. return cookies;
  425. }
  426. }
  427. public bool HasEntityBody
  428. {
  429. get { return (content_length > 0 || is_chunked); }
  430. }
  431. public QueryParamCollection Headers
  432. {
  433. get { return headers; }
  434. }
  435. public string HttpMethod
  436. {
  437. get { return method; }
  438. }
  439. public Stream InputStream
  440. {
  441. get
  442. {
  443. if (input_stream == null)
  444. {
  445. if (is_chunked || content_length > 0)
  446. input_stream = context.Connection.GetRequestStream(is_chunked, content_length);
  447. else
  448. input_stream = Stream.Null;
  449. }
  450. return input_stream;
  451. }
  452. }
  453. public bool IsAuthenticated
  454. {
  455. get { return false; }
  456. }
  457. public bool IsLocal
  458. {
  459. get { return RemoteEndPoint.IpAddress.Equals(IpAddressInfo.Loopback) || RemoteEndPoint.IpAddress.Equals(IpAddressInfo.IPv6Loopback) || LocalEndPoint.IpAddress.Equals(RemoteEndPoint.IpAddress); }
  460. }
  461. public bool IsSecureConnection
  462. {
  463. get { return context.Connection.IsSecure; }
  464. }
  465. public bool KeepAlive
  466. {
  467. get
  468. {
  469. if (ka_set)
  470. return keep_alive;
  471. ka_set = true;
  472. // 1. Connection header
  473. // 2. Protocol (1.1 == keep-alive by default)
  474. // 3. Keep-Alive header
  475. string cnc = headers["Connection"];
  476. if (!String.IsNullOrEmpty(cnc))
  477. {
  478. keep_alive = (0 == String.Compare(cnc, "keep-alive", StringComparison.OrdinalIgnoreCase));
  479. }
  480. else if (version == HttpVersion.Version11)
  481. {
  482. keep_alive = true;
  483. }
  484. else
  485. {
  486. cnc = headers["keep-alive"];
  487. if (!String.IsNullOrEmpty(cnc))
  488. keep_alive = (0 != String.Compare(cnc, "closed", StringComparison.OrdinalIgnoreCase));
  489. }
  490. return keep_alive;
  491. }
  492. }
  493. public IpEndPointInfo LocalEndPoint
  494. {
  495. get { return context.Connection.LocalEndPoint; }
  496. }
  497. public Version ProtocolVersion
  498. {
  499. get { return version; }
  500. }
  501. public QueryParamCollection QueryString
  502. {
  503. get { return query_string; }
  504. }
  505. public string RawUrl
  506. {
  507. get { return raw_url; }
  508. }
  509. public IpEndPointInfo RemoteEndPoint
  510. {
  511. get { return context.Connection.RemoteEndPoint; }
  512. }
  513. public Guid RequestTraceIdentifier
  514. {
  515. get { return Guid.Empty; }
  516. }
  517. public Uri Url
  518. {
  519. get { return url; }
  520. }
  521. public Uri UrlReferrer
  522. {
  523. get { return referrer; }
  524. }
  525. public string UserAgent
  526. {
  527. get { return headers["user-agent"]; }
  528. }
  529. public string UserHostAddress
  530. {
  531. get { return LocalEndPoint.ToString(); }
  532. }
  533. public string UserHostName
  534. {
  535. get { return headers["host"]; }
  536. }
  537. public string[] UserLanguages
  538. {
  539. get { return user_languages; }
  540. }
  541. public string ServiceName
  542. {
  543. get
  544. {
  545. return null;
  546. }
  547. }
  548. private bool _websocketRequestWasSet;
  549. private bool _websocketRequest;
  550. /// <summary>
  551. /// Gets a value indicating whether the request is a WebSocket connection request.
  552. /// </summary>
  553. /// <value>
  554. /// <c>true</c> if the request is a WebSocket connection request; otherwise, <c>false</c>.
  555. /// </value>
  556. public bool IsWebSocketRequest
  557. {
  558. get
  559. {
  560. if (!_websocketRequestWasSet)
  561. {
  562. _websocketRequest = method == "GET" &&
  563. version > HttpVersion.Version10 &&
  564. headers.Contains("Upgrade", "websocket") &&
  565. headers.Contains("Connection", "Upgrade");
  566. _websocketRequestWasSet = true;
  567. }
  568. return _websocketRequest;
  569. }
  570. }
  571. public Task<ICertificate> GetClientCertificateAsync()
  572. {
  573. return Task.FromResult<ICertificate>(null);
  574. }
  575. }
  576. }