/src/Manos/Manos.Http/HttpHeaders.cs

http://github.com/jacksonh/manos · C# · 330 lines · 239 code · 62 blank · 29 comment · 71 complexity · db86b9e3a057ee0d7fc2dd400ef0e087 MD5 · raw file

  1. //
  2. // Copyright (C) 2010 Jackson Harper (jackson@manosdemono.com)
  3. //
  4. // Permission is hereby granted, free of charge, to any person obtaining
  5. // a copy of this software and associated documentation files (the
  6. // "Software"), to deal in the Software without restriction, including
  7. // without limitation the rights to use, copy, modify, merge, publish,
  8. // distribute, sublicense, and/or sell copies of the Software, and to
  9. // permit persons to whom the Software is furnished to do so, subject to
  10. // the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be
  13. // included in all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  16. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  17. // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  18. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  19. // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  20. // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  21. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  22. //
  23. //
  24. using System;
  25. using System.IO;
  26. using System.Text;
  27. using System.Net;
  28. using System.Net.Sockets;
  29. using System.Globalization;
  30. using System.Collections;
  31. using System.Collections.Generic;
  32. using System.Collections.Specialized;
  33. namespace Manos.Http {
  34. public class HttpHeaders {
  35. public static readonly string CONTENT_LENGTH_KEY = "Content-Length";
  36. private long? content_length;
  37. private Encoding encoding;
  38. Dictionary<string,string> items = new Dictionary<string,string> ();
  39. public HttpHeaders ()
  40. {
  41. }
  42. public long? ContentLength {
  43. get { return content_length; }
  44. set {
  45. if (value < 0)
  46. throw new ArgumentException ("value");
  47. content_length = value;
  48. if (value == null) {
  49. items.Remove (CONTENT_LENGTH_KEY);
  50. return;
  51. }
  52. items [CONTENT_LENGTH_KEY] = value.ToString ();
  53. }
  54. }
  55. public Encoding ContentEncoding {
  56. get {
  57. if (encoding == null)
  58. SetEncodingInternal ();
  59. return encoding;
  60. }
  61. set {
  62. encoding = value;
  63. }
  64. }
  65. private void SetEncodingInternal ()
  66. {
  67. string content;
  68. if (!TryGetNormalizedValue ("Content-Type", out content)) {
  69. encoding = Encoding.ASCII;
  70. return;
  71. }
  72. string charset = HttpHeaders.GetAttribute (content, "; charset=");
  73. if (charset == null) {
  74. encoding = Encoding.Default;
  75. return;
  76. }
  77. try {
  78. encoding = Encoding.GetEncoding (charset);
  79. } catch (Exception e) {
  80. Console.Error.WriteLine ("[non-fatal] Exception while setting encoding:");
  81. Console.Error.WriteLine (e);
  82. encoding = Encoding.Default;
  83. }
  84. }
  85. public Dictionary<string,string>.KeyCollection Keys {
  86. get { return items.Keys; }
  87. }
  88. public string this [string name] {
  89. get {
  90. if (name == null)
  91. throw new ArgumentNullException ("name");
  92. return items [NormalizeName (name)];
  93. }
  94. }
  95. public int Count {
  96. get { return items.Count; }
  97. }
  98. public bool TryGetValue (string key, out string value)
  99. {
  100. return TryGetNormalizedValue (NormalizeName (key), out value);
  101. }
  102. public bool TryGetNormalizedValue (string key, out string value)
  103. {
  104. return items.TryGetValue (key, out value);
  105. }
  106. public void Parse (TextReader reader)
  107. {
  108. string line = reader.ReadLine ();
  109. while (line != null) {
  110. int line_end = line.Length - 1;
  111. if (String.IsNullOrEmpty (line))
  112. return;
  113. if (Char.IsWhiteSpace (line [0]))
  114. throw new HttpException ("Malformed HTTP header. Found whitespace before data.");
  115. while (Char.IsWhiteSpace (line [line_end])) {
  116. line_end--;
  117. if (line_end == 0)
  118. throw new HttpException ("Malformed HTTP header. No data found.");
  119. }
  120. int colon = line.IndexOf (':');
  121. if (colon <= 0)
  122. throw new HttpException ("Malformed HTTP header. No colon found.");
  123. if (colon >= line_end)
  124. throw new HttpException ("Malformed HTTP header. No value found.");
  125. string value = line.Substring (colon + 1, line_end - colon).TrimStart ();
  126. if (value.Length == 0)
  127. throw new HttpException ("Malformed HTTP header. No Value found.");
  128. string key = line.Substring (0, colon);
  129. //
  130. // If the next line starts with whitespace its part of the current value
  131. //
  132. line = reader.ReadLine ();
  133. while (line != null && line.Length > 0 && Char.IsWhiteSpace (line [0])) {
  134. value += " " + line.Trim ();
  135. line = reader.ReadLine ();
  136. }
  137. SetHeader (key, value);
  138. }
  139. }
  140. public void Write (StringBuilder builder, ICollection<HttpCookie> cookies, Encoding encoding)
  141. {
  142. foreach (var h in items.Keys) {
  143. string header = (string) h;
  144. builder.Append (header);
  145. builder.Append (": ");
  146. builder.Append (items [header]);
  147. builder.Append ("\r\n");
  148. }
  149. if (cookies != null) {
  150. foreach (HttpCookie cookie in cookies) {
  151. builder.Append (cookie.ToHeaderString ());
  152. }
  153. }
  154. builder.Append ("\r\n");
  155. }
  156. public void SetHeader (string name, string value)
  157. {
  158. if (name == null)
  159. throw new ArgumentNullException ("name");
  160. name = NormalizeName (name);
  161. SetNormalizedHeader (name, value);
  162. }
  163. public void SetNormalizedHeader (string name, string value)
  164. {
  165. if (!IsValidHeaderName (name))
  166. throw new ArgumentException (String.Format ("Invalid header '{0}'.", name));
  167. if (name == CONTENT_LENGTH_KEY) {
  168. SetContentLength (value);
  169. return;
  170. }
  171. if (value == null) {
  172. items.Remove (name);
  173. return;
  174. }
  175. items [name] = value;
  176. }
  177. public static string NormalizeName (string name)
  178. {
  179. if (String.IsNullOrEmpty (name))
  180. throw new ArgumentException ("name", "name must be a non-null non-empty string");
  181. StringBuilder res = null;
  182. if (Char.IsLower (name [0])) {
  183. res = new StringBuilder (name);
  184. res [0] = Char.ToUpper (name [0], CultureInfo.InvariantCulture);
  185. }
  186. char p = name [0];
  187. for (int i = 1; i < name.Length; i++) {
  188. char c = name [i];
  189. if (p == '-' && Char.IsLower (c)) {
  190. if (res == null)
  191. res = new StringBuilder (name);
  192. res [i] = Char.ToUpper (c, CultureInfo.InvariantCulture);
  193. } else if (p != '-' && Char.IsUpper (c)) {
  194. if (res == null)
  195. res = new StringBuilder (name);
  196. res [i] = Char.ToLower (c, CultureInfo.InvariantCulture);
  197. }
  198. p = c;
  199. }
  200. if (res != null)
  201. return res.ToString ();
  202. return name;
  203. }
  204. public bool IsValidHeaderName (string name)
  205. {
  206. // TODO: What more can I do here?
  207. if (name.Length == 0)
  208. return false;
  209. return true;
  210. }
  211. public void SetContentLength (string value)
  212. {
  213. if (value == null) {
  214. items.Remove (CONTENT_LENGTH_KEY);
  215. content_length = null;
  216. return;
  217. }
  218. int cl;
  219. if (!Int32.TryParse (value, out cl))
  220. throw new ArgumentException ("Malformed HTTP Header, invalid Content-Length value.", "value");
  221. if (cl < 0)
  222. throw new ArgumentException ("Content-Length must be a positive integer.", "value");
  223. ContentLength = cl;
  224. }
  225. /// from mono's System.Web/HttpRequest.cs
  226. public static string GetAttribute (string header_value, string attr)
  227. {
  228. int start = header_value.IndexOf (attr);
  229. if (start == -1)
  230. return null;
  231. start += attr.Length;
  232. if (start >= header_value.Length)
  233. return null;
  234. char ending = header_value [start];
  235. if (ending != '"')
  236. ending = ' ';
  237. int end = header_value.IndexOf (ending, start + 1);
  238. if (end == -1) {
  239. // Use the full string unless its a unclosed quote
  240. // TODO: What about multiline values, can they be broken across lines?
  241. return (ending == '"') ? null : header_value.Substring (start);
  242. }
  243. return header_value.Substring (start + 1, end - start - 1);
  244. }
  245. /// from mono's System.Web.Util/HttpEncoder.cs
  246. private static string EncodeHeaderString (string input)
  247. {
  248. StringBuilder sb = null;
  249. char ch;
  250. for (int i = 0; i < input.Length; i++) {
  251. ch = input [i];
  252. if ((ch < 32 && ch != 9) || ch == 127)
  253. StringBuilderAppend (String.Format ("%{0:x2}", (int)ch), ref sb);
  254. }
  255. if (sb != null)
  256. return sb.ToString ();
  257. return input;
  258. }
  259. private static void StringBuilderAppend (string s, ref StringBuilder sb)
  260. {
  261. if (sb == null)
  262. sb = new StringBuilder (s);
  263. else
  264. sb.Append (s);
  265. }
  266. }
  267. }