HttpListenerRequest.cs 21 KB

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