/src/Manos/Manos.Http/HttpMultiPartFormDataHandler.cs

http://github.com/jacksonh/manos · C# · 335 lines · 234 code · 72 blank · 29 comment · 48 complexity · 39759e828776395aa506e2123e060db0 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.Collections;
  28. using System.Collections.Generic;
  29. using Manos.IO;
  30. namespace Manos.Http {
  31. public class HttpMultiPartFormDataHandler : IHttpBodyHandler {
  32. private enum State {
  33. Error,
  34. InHeaderKey,
  35. InHeaderValue,
  36. PostHeader1,
  37. PostHeader2,
  38. InFormData,
  39. InFileData,
  40. InBoundary,
  41. PostBoundary1,
  42. PostBoundaryComplete,
  43. Finished
  44. }
  45. private int index;
  46. private State state;
  47. private State previous_state;
  48. private bool not_boundary;
  49. private string boundary;
  50. private Encoding encoding;
  51. private IUploadedFileCreator file_creator;
  52. private List<byte> header_key = new List<byte> ();
  53. private List<byte> header_value = new List<byte> ();
  54. private List<byte> boundary_buffer = new List<byte> ();
  55. private string current_name;
  56. private string current_filename;
  57. private string content_type;
  58. private UploadedFile uploaded_file;
  59. private List<byte> form_data = new List<byte> ();
  60. private char[] quotation_mark = {'\"'};
  61. public HttpMultiPartFormDataHandler (string boundary, Encoding encoding, IUploadedFileCreator file_creator)
  62. {
  63. this.boundary = "--" + boundary.TrimStart(quotation_mark).TrimEnd(quotation_mark);
  64. this.encoding = encoding;
  65. this.file_creator = file_creator;
  66. state = State.InBoundary;
  67. }
  68. public void HandleData (HttpEntity entity, ByteBuffer data, int pos, int len)
  69. {
  70. // string str_data = encoding.GetString (data.Bytes, pos, len);
  71. byte [] str_data = data.Bytes;
  72. int begin = pos;
  73. int end = begin + len;
  74. pos = begin - 1;
  75. while (pos < end - 1 && state != State.Finished) {
  76. byte c = str_data [++pos];
  77. switch (state) {
  78. case State.InBoundary:
  79. if (index == boundary.Length - 1) {
  80. boundary_buffer.Clear ();
  81. // Flush any data
  82. FinishFormData (entity);
  83. FinishFileData (entity);
  84. state = State.PostBoundary1;
  85. index = 0;
  86. break;
  87. }
  88. boundary_buffer.Add (c);
  89. if (c != boundary [index]) {
  90. // Copy the boundary buffer to the beginning and restart parsing there
  91. MemoryStream stream = new MemoryStream ();
  92. stream.Write (boundary_buffer.ToArray (), 0, boundary_buffer.Count);
  93. stream.Write (str_data, pos + 1, end - pos - 1);
  94. str_data = stream.ToArray ();
  95. pos = -1;
  96. end = str_data.Length;
  97. not_boundary = true;
  98. boundary_buffer.Clear ();
  99. state = previous_state;
  100. index = 0;
  101. // continue instead of break so not_boundary is not reset
  102. continue;
  103. }
  104. ++index;
  105. break;
  106. case State.PostBoundary1:
  107. if (c == '-') {
  108. state = State.PostBoundaryComplete;
  109. break;
  110. }
  111. if (c == '\r')
  112. break;
  113. if (c == '\n') {
  114. state = State.InHeaderKey;
  115. break;
  116. }
  117. throw new Exception (String.Format ("Invalid post boundary char '{0}'", c));
  118. case State.PostBoundaryComplete:
  119. if (c != '-')
  120. throw new Exception (String.Format ("Invalid char '{0}' in boundary complete.", c));
  121. state = State.Finished;
  122. break;
  123. case State.InHeaderKey:
  124. if (c == '\n') {
  125. state = current_filename == null ? State.InFormData : State.InFileData;
  126. break;
  127. }
  128. if (c == ':') {
  129. state = State.InHeaderValue;
  130. break;
  131. }
  132. header_key.Add (c);
  133. break;
  134. case State.InHeaderValue:
  135. if (c == '\r') {
  136. state = State.PostHeader1;
  137. break;
  138. }
  139. header_value.Add (c);
  140. break;
  141. case State.PostHeader1:
  142. if (c != '\n')
  143. throw new Exception (String.Format ("Invalid char '{0}' in post header 1.", c));
  144. state = State.PostHeader2;
  145. break;
  146. case State.PostHeader2:
  147. HandleHeader (entity);
  148. header_key.Clear ();
  149. header_value.Clear ();
  150. state = State.InHeaderKey;
  151. break;
  152. case State.InFormData:
  153. if (CheckStartingBoundary (str_data, pos))
  154. break;
  155. form_data.Add (c);
  156. break;
  157. case State.InFileData:
  158. if (CheckStartingBoundary (str_data, pos))
  159. break;;
  160. if (uploaded_file != null)
  161. uploaded_file.Contents.WriteByte (c);
  162. break;
  163. default:
  164. throw new Exception (String.Format ("Unhandled state: {0}", state));
  165. }
  166. not_boundary = false;
  167. }
  168. }
  169. private bool CheckStartingBoundary (byte [] str_data, int pos)
  170. {
  171. if (not_boundary)
  172. return false;
  173. if (pos >= str_data.Length)
  174. return false;
  175. bool res = str_data [pos] == boundary [0];
  176. if (res) {
  177. boundary_buffer.Clear ();
  178. boundary_buffer.Add (str_data [pos]);
  179. index = 1;
  180. previous_state = state;
  181. state = State.InBoundary;
  182. }
  183. return res;
  184. }
  185. private void HandleHeader (HttpEntity entity)
  186. {
  187. string key = encoding.GetString (header_key.ToArray ());
  188. string value = encoding.GetString (header_value.ToArray ());
  189. if (String.Compare(key,"Content-Disposition",true) == 0)
  190. ParseContentDisposition (value);
  191. else if (String.Compare(key,"Content-Type",true) == 0)
  192. ParseContentType (value);
  193. }
  194. public void Finish (HttpEntity entity)
  195. {
  196. FinishFormData (entity);
  197. FinishFileData (entity);
  198. }
  199. private void FinishFormData (HttpEntity entity)
  200. {
  201. if (form_data.Count <= 2)
  202. return;
  203. // Chop the \r\n off the end
  204. form_data.RemoveRange (form_data.Count - 2, 2);
  205. string data = encoding.GetString (form_data.ToArray ());
  206. entity.PostData.Set (current_name, data);
  207. form_data.Clear ();
  208. }
  209. private void FinishFileData (HttpEntity entity)
  210. {
  211. if (uploaded_file == null)
  212. return;
  213. // Chop off the \r\n that gets appended before the boundary marker
  214. uploaded_file.Contents.SetLength (uploaded_file.Contents.Position - 2);
  215. uploaded_file.Finish ();
  216. if (uploaded_file.Length > 0)
  217. entity.Files.Add (current_name, uploaded_file);
  218. uploaded_file = null;
  219. }
  220. public void ParseContentDisposition (string str)
  221. {
  222. current_name = GetContentDispositionAttribute (str, "name");
  223. current_filename = GetContentDispositionAttributeWithEncoding (str, "filename");
  224. if (!String.IsNullOrEmpty (current_filename))
  225. uploaded_file = file_creator.Create (current_filename);
  226. }
  227. public void ParseContentType (string str)
  228. {
  229. content_type = str.Trim ();
  230. }
  231. public static string GetContentDispositionAttribute (string l, string name)
  232. {
  233. int idx = l.IndexOf (name + "=\"", StringComparison.InvariantCulture);
  234. if (idx < 0)
  235. return null;
  236. int begin = idx + name.Length + "=\"".Length;
  237. int end = l.IndexOf ('"', begin);
  238. if (end < 0)
  239. return null;
  240. if (begin == end)
  241. return String.Empty;
  242. return l.Substring (begin, end - begin);
  243. }
  244. public string GetContentDispositionAttributeWithEncoding (string l, string name)
  245. {
  246. int idx = l.IndexOf (name + "=\"", StringComparison.InvariantCulture);
  247. if (idx < 0)
  248. return null;
  249. int begin = idx + name.Length + "=\"".Length;
  250. int end = l.IndexOf ('"', begin);
  251. if (end < 0)
  252. return null;
  253. if (begin == end)
  254. return String.Empty;
  255. string temp = l.Substring (begin, end - begin);
  256. byte [] source = new byte [temp.Length];
  257. for (int i = temp.Length - 1; i >= 0; i--)
  258. source [i] = (byte) temp [i];
  259. return encoding.GetString (source);
  260. }
  261. }
  262. }