/src/DotNetty.Codecs.Http/Cookies/ClientCookieDecoder.cs

https://github.com/Azure/DotNetty · C# · 266 lines · 215 code · 33 blank · 18 comment · 71 complexity · 8d7ad41198ec061ae4f910d4b07f25bf MD5 · raw file

  1. // Copyright (c) Microsoft. All rights reserved.
  2. // Licensed under the MIT license. See LICENSE file in the project root for full license information.
  3. namespace DotNetty.Codecs.Http.Cookies
  4. {
  5. using System;
  6. using System.Diagnostics;
  7. using System.Diagnostics.Contracts;
  8. using DotNetty.Common.Utilities;
  9. public sealed class ClientCookieDecoder : CookieDecoder
  10. {
  11. // Strict encoder that validates that name and value chars are in the valid scope
  12. // defined in RFC6265
  13. public static readonly ClientCookieDecoder StrictDecoder = new ClientCookieDecoder(true);
  14. // Lax instance that doesn't validate name and value
  15. public static readonly ClientCookieDecoder LaxDecoder = new ClientCookieDecoder(false);
  16. ClientCookieDecoder(bool strict) : base(strict)
  17. {
  18. }
  19. public ICookie Decode(string header)
  20. {
  21. Contract.Requires(header != null);
  22. int headerLen = header.Length;
  23. if (headerLen == 0)
  24. {
  25. return null;
  26. }
  27. CookieBuilder cookieBuilder = null;
  28. //loop:
  29. for (int i = 0;;)
  30. {
  31. // Skip spaces and separators.
  32. for (;;)
  33. {
  34. if (i == headerLen)
  35. {
  36. goto loop;
  37. }
  38. char c = header[i];
  39. if (c == ',')
  40. {
  41. // Having multiple cookies in a single Set-Cookie header is
  42. // deprecated, modern browsers only parse the first one
  43. goto loop;
  44. }
  45. else if (c == '\t' || c == '\n' || c == 0x0b || c == '\f'
  46. || c == '\r' || c == ' ' || c == ';')
  47. {
  48. i++;
  49. continue;
  50. }
  51. break;
  52. }
  53. int nameBegin = i;
  54. int nameEnd;
  55. int valueBegin;
  56. int valueEnd;
  57. for (;;)
  58. {
  59. char curChar = header[i];
  60. if (curChar == ';')
  61. {
  62. // NAME; (no value till ';')
  63. nameEnd = i;
  64. valueBegin = valueEnd = -1;
  65. break;
  66. }
  67. else if (curChar == '=')
  68. {
  69. // NAME=VALUE
  70. nameEnd = i;
  71. i++;
  72. if (i == headerLen)
  73. {
  74. // NAME= (empty value, i.e. nothing after '=')
  75. valueBegin = valueEnd = 0;
  76. break;
  77. }
  78. valueBegin = i;
  79. // NAME=VALUE;
  80. int semiPos = header.IndexOf(';', i);
  81. valueEnd = i = semiPos > 0 ? semiPos : headerLen;
  82. break;
  83. }
  84. else
  85. {
  86. i++;
  87. }
  88. if (i == headerLen)
  89. {
  90. // NAME (no value till the end of string)
  91. nameEnd = headerLen;
  92. valueBegin = valueEnd = -1;
  93. break;
  94. }
  95. }
  96. if (valueEnd > 0 && header[valueEnd - 1] == ',')
  97. {
  98. // old multiple cookies separator, skipping it
  99. valueEnd--;
  100. }
  101. if (cookieBuilder == null)
  102. {
  103. // cookie name-value pair
  104. DefaultCookie cookie = this.InitCookie(header, nameBegin, nameEnd, valueBegin, valueEnd);
  105. if (cookie == null)
  106. {
  107. return null;
  108. }
  109. cookieBuilder = new CookieBuilder(cookie, header);
  110. }
  111. else
  112. {
  113. // cookie attribute
  114. cookieBuilder.AppendAttribute(nameBegin, nameEnd, valueBegin, valueEnd);
  115. }
  116. }
  117. loop:
  118. Debug.Assert(cookieBuilder != null);
  119. return cookieBuilder.Cookie();
  120. }
  121. sealed class CookieBuilder
  122. {
  123. readonly string header;
  124. readonly DefaultCookie cookie;
  125. string domain;
  126. string path;
  127. long maxAge = long.MinValue;
  128. int expiresStart;
  129. int expiresEnd;
  130. bool secure;
  131. bool httpOnly;
  132. internal CookieBuilder(DefaultCookie cookie, string header)
  133. {
  134. this.cookie = cookie;
  135. this.header = header;
  136. }
  137. long MergeMaxAgeAndExpires()
  138. {
  139. // max age has precedence over expires
  140. if (this.maxAge != long.MinValue)
  141. {
  142. return this.maxAge;
  143. }
  144. else if (IsValueDefined(this.expiresStart, this.expiresEnd))
  145. {
  146. DateTime? expiresDate = DateFormatter.ParseHttpDate(this.header, this.expiresStart, this.expiresEnd);
  147. if (expiresDate != null)
  148. {
  149. return (expiresDate.Value.Ticks - DateTime.UtcNow.Ticks) / TimeSpan.TicksPerSecond;
  150. }
  151. }
  152. return long.MinValue;
  153. }
  154. internal ICookie Cookie()
  155. {
  156. this.cookie.Domain = this.domain;
  157. this.cookie.Path = this.path;
  158. this.cookie.MaxAge = this.MergeMaxAgeAndExpires();
  159. this.cookie.IsSecure = this.secure;
  160. this.cookie.IsHttpOnly = this.httpOnly;
  161. return this.cookie;
  162. }
  163. public void AppendAttribute(int keyStart, int keyEnd, int valueStart, int valueEnd)
  164. {
  165. int length = keyEnd - keyStart;
  166. if (length == 4)
  167. {
  168. this.Parse4(keyStart, valueStart, valueEnd);
  169. }
  170. else if (length == 6)
  171. {
  172. this.Parse6(keyStart, valueStart, valueEnd);
  173. }
  174. else if (length == 7)
  175. {
  176. this.Parse7(keyStart, valueStart, valueEnd);
  177. }
  178. else if (length == 8)
  179. {
  180. this.Parse8(keyStart);
  181. }
  182. }
  183. void Parse4(int nameStart, int valueStart, int valueEnd)
  184. {
  185. if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.Path, 0, 4))
  186. {
  187. this.path = this.ComputeValue(valueStart, valueEnd);
  188. }
  189. }
  190. void Parse6(int nameStart, int valueStart, int valueEnd)
  191. {
  192. if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.Domain, 0, 5))
  193. {
  194. this.domain = this.ComputeValue(valueStart, valueEnd);
  195. }
  196. else if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.Secure, 0, 5))
  197. {
  198. this.secure = true;
  199. }
  200. }
  201. void SetMaxAge(string value)
  202. {
  203. if (long.TryParse(value, out long v))
  204. {
  205. this.maxAge = Math.Max(v, 0);
  206. }
  207. }
  208. void Parse7(int nameStart, int valueStart, int valueEnd)
  209. {
  210. if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.Expires, 0, 7))
  211. {
  212. this.expiresStart = valueStart;
  213. this.expiresEnd = valueEnd;
  214. }
  215. else if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.MaxAge, 0, 7))
  216. {
  217. this.SetMaxAge(this.ComputeValue(valueStart, valueEnd));
  218. }
  219. }
  220. void Parse8(int nameStart)
  221. {
  222. if (CharUtil.RegionMatchesIgnoreCase(this.header, nameStart, CookieHeaderNames.HttpOnly, 0, 8))
  223. {
  224. this.httpOnly = true;
  225. }
  226. }
  227. static bool IsValueDefined(int valueStart, int valueEnd) => valueStart != -1 && valueStart != valueEnd;
  228. string ComputeValue(int valueStart, int valueEnd) => IsValueDefined(valueStart, valueEnd)
  229. ? this.header.Substring(valueStart, valueEnd - valueStart)
  230. : null;
  231. }
  232. }
  233. }