PageRenderTime 271ms CodeModel.GetById 255ms app.highlight 13ms RepoModel.GetById 1ms app.codeStats 0ms

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