diff --git a/src/Nancy.MSBuild/Nancy.csproj b/src/Nancy.MSBuild/Nancy.csproj index 01e96c1510..5cff659f7b 100644 --- a/src/Nancy.MSBuild/Nancy.csproj +++ b/src/Nancy.MSBuild/Nancy.csproj @@ -1004,15 +1004,6 @@ HttpFile.cs - - HttpMultipart.cs - - - HttpMultipartBoundary.cs - - - HttpMultipartBuffer.cs - HttpStatusCode.cs @@ -1205,9 +1196,6 @@ Session\Session.cs - - HttpMultipartSubStream.cs - TinyIoc\TinyIoC.cs @@ -1364,6 +1352,33 @@ Configuration\ConfigurationException.cs + + HttpMultipart\BinaryStreamStack.cs + + + HttpMultipart\FilePart.cs + + + HttpMultipart\MultipartFormDataParser.cs + + + HttpMultipart\MultipartParseException.cs + + + HttpMultipart\MultipartStreamPart.cs + + + HttpMultipart\ParameterPart.cs + + + HttpMultipart\RebufferableBinaryReader.cs + + + HttpMultipart\StreamingMultipartFormDataParser.cs + + + HttpMultipart\SubsequenceFinder.cs + diff --git a/src/Nancy/HttpFile.cs b/src/Nancy/HttpFile.cs index 0435d8293a..1ededc9dda 100644 --- a/src/Nancy/HttpFile.cs +++ b/src/Nancy/HttpFile.cs @@ -7,16 +7,6 @@ /// public class HttpFile { - /// - /// Initializes a new instance of the class, - /// using the provided . - /// - /// The that contains the file information. - public HttpFile(HttpMultipartBoundary boundary) - : this(boundary.ContentType, boundary.Filename, boundary.Value, boundary.Name) - { - } - /// /// Initializes a new instance of the class, /// using the provided values diff --git a/src/Nancy/HttpMultipart/BinaryStreamStack.cs b/src/Nancy/HttpMultipart/BinaryStreamStack.cs new file mode 100644 index 0000000000..759cc91ed5 --- /dev/null +++ b/src/Nancy/HttpMultipart/BinaryStreamStack.cs @@ -0,0 +1,404 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2013 Jake Woods +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies +// or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// Jake Woods +// +// Provides character based and byte based stream-like read operations over multiple +// streams and provides methods to add data to the front of the buffer. +// +// -------------------------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Nancy.HttpMultipart //HttpMultipartParser +{ + /// + /// Provides character based and byte based stream-like read operations over multiple + /// streams and provides methods to add data to the front of the buffer. + /// + internal class BinaryStreamStack + { + #region Fields + + /// + /// Holds the streams to read from, the stream on the top of the + /// stack will be read first. + /// + private readonly Stack streams = new Stack(); + + #endregion + + #region Constructors and Destructors + + /// + /// Initializes a new instance of the class with the default + /// encoding of UTF8. + /// + public BinaryStreamStack() + : this(Encoding.UTF8) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The encoding to use for character based operations. + /// + public BinaryStreamStack(Encoding encoding) + { + CurrentEncoding = encoding; + } + + #endregion + + #region Public Properties + + /// + /// Gets or sets the current encoding. + /// + public Encoding CurrentEncoding { get; set; } + + #endregion + + #region Public Methods and Operators + + /// + /// Returns true if there is any data left to read. + /// + /// + /// True or false. + /// + public bool HasData() + { + return streams.Any(); + } + + /// + /// Returns the reader on the top of the stack but does not remove it. + /// + /// + /// The . + /// + public BinaryReader Peek() + { + return streams.Peek(); + } + + /// + /// Returns the reader on the top of the stack and removes it + /// + /// + /// The . + /// + public BinaryReader Pop() + { + return streams.Pop(); + } + + /// + /// Pushes data to the front of the stack. The most recently pushed data will + /// be read first. + /// + /// + /// The data to add to the stack. + /// + public void Push(byte[] data) + { + streams.Push(new BinaryReader(new MemoryStream(data), CurrentEncoding)); + } + + /// + /// Reads a single byte as an integer from the stack. Returns -1 if no + /// data is left to read. + /// + /// + /// The that was read. + /// + public int Read() + { + BinaryReader top = streams.Peek(); + + int value; + while ((value = top.Read()) == -1) + { + top.Dispose(); + streams.Pop(); + + if (!streams.Any()) + { + return -1; + } + + top = streams.Peek(); + } + + return value; + } + + /// + /// Reads the specified number of bytes from the stack, starting from a specified point in the byte array. + /// + /// + /// The buffer to read data into. + /// + /// + /// The index of buffer to start reading into. + /// + /// + /// The number of bytes to read into the buffer. + /// + /// + /// The number of bytes read into buffer. This might be less than the number of bytes requested if that many bytes are not available, + /// or it might be zero if the end of the stream is reached. + /// + public int Read(byte[] buffer, int index, int count) + { + if (!HasData()) + { + return 0; + } + + // Read through all the stream untill we exhaust them + // or untill count is satisfied + int amountRead = 0; + BinaryReader top = streams.Peek(); + while (amountRead < count && streams.Any()) + { + int read = top.Read(buffer, index + amountRead, count - amountRead); + if (read == 0) + { + if ((top = NextStream()) == null) + { + return amountRead; + } + } + else + { + amountRead += read; + } + } + + return amountRead; + } + + /// + /// Reads the specified number of characters from the stack, starting from a specified point in the byte array. + /// + /// + /// The buffer to read data into. + /// + /// + /// The index of buffer to start reading into. + /// + /// + /// The number of characters to read into the buffer. + /// + /// + /// The number of characters read into buffer. This might be less than the number of bytes requested if that many bytes are not available, + /// or it might be zero if the end of the stream is reached. + /// + public int Read(char[] buffer, int index, int count) + { + if (!HasData()) + { + return 0; + } + + // Read through all the stream untill we exhaust them + // or untill count is satisfied + int amountRead = 0; + BinaryReader top = streams.Peek(); + while (amountRead < count && streams.Any()) + { + int read = top.Read(buffer, index + amountRead, count - amountRead); + if (read == 0) + { + if ((top = NextStream()) == null) + { + return amountRead; + } + } + else + { + amountRead += read; + } + } + + return amountRead; + } + + /// + /// Reads the specified number of characters from the stack, starting from a specified point in the byte array. + /// + /// + /// A byte array containing all the data up to but not including the next newline in the stack. + /// + public byte[] ReadByteLine() + { + bool dummy; + return ReadByteLine(out dummy); + } + + /// + /// Reads a line from the stack delimited by the newline for this platform. The newline + /// characters will not be included in the stream + /// + /// + /// This will be set to true if we did not end on a newline but instead found the end of + /// our data. + /// + /// + /// The containing the line. + /// + public byte[] ReadByteLine(out bool hitStreamEnd) + { + hitStreamEnd = false; + if (!HasData()) + { + // No streams, no data! + return null; + } + + // This is horribly inefficient, consider profiling here if + // it becomes an issue. + BinaryReader top = streams.Peek(); + byte[] ignore = CurrentEncoding.GetBytes(new[] {'\r'}); + byte[] search = CurrentEncoding.GetBytes(new[] {'\n'}); + int searchPos = 0; + var builder = new MemoryStream(); + + while (true) + { + // First we need to read a byte from one of the streams + var bytes = new byte[search.Length]; + int amountRead = top.Read(bytes, 0, bytes.Length); + while (amountRead == 0) + { + streams.Pop(); + if (!streams.Any()) + { + hitStreamEnd = true; + return builder.ToArray(); + } + + top.Dispose(); + top = streams.Peek(); + amountRead = top.Read(bytes, 0, bytes.Length); + } + + // Now we've got some bytes, we need to check it against the search array. + foreach (byte b in bytes) + { + if (ignore.Contains(b)) + { + continue; + } + + if (b == search[searchPos]) + { + searchPos += 1; + } + else + { + // We only want to append the information if it's + // not part of the newline sequence + if (searchPos != 0) + { + byte[] append = search.Take(searchPos).ToArray(); + builder.Write(append, 0, append.Length); + } + + builder.Write(new[] {b}, 0, 1); + searchPos = 0; + } + + // Finally if we've found our string + if (searchPos == search.Length) + { + return builder.ToArray(); + } + } + } + } + + /// + /// Reads a line from the stack delimited by the newline for this platform. The newline + /// characters will not be included in the stream + /// + /// + /// The containing the line. + /// + public string ReadLine() + { + bool dummy; + return ReadLine(out dummy); + } + + /// + /// Reads a line from the stack delimited by the newline for this platform. The newline + /// characters will not be included in the stream + /// + /// + /// This will be set to true if we did not end on a newline but instead found the end of + /// our data. + /// + /// + /// The containing the line. + /// + public string ReadLine(out bool hitStreamEnd) + { + bool foundEnd; + byte[] result = ReadByteLine(out foundEnd); + hitStreamEnd = foundEnd; + + if (result == null) + { + return null; + } + + return CurrentEncoding.GetString(result); + } + + #endregion + + #region Methods + + /// + /// Removes the current reader from the stack and ensures it is correctly + /// destroyed and then returns the next available reader. If no reader + /// is available this method returns null. + /// + /// + /// The next reader. + /// + private BinaryReader NextStream() + { + BinaryReader top = streams.Pop(); + top.Dispose(); + + return streams.Any() ? streams.Peek() : null; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Nancy/HttpMultipart/FilePart.cs b/src/Nancy/HttpMultipart/FilePart.cs new file mode 100644 index 0000000000..c6a148680b --- /dev/null +++ b/src/Nancy/HttpMultipart/FilePart.cs @@ -0,0 +1,115 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2013 Jake Woods +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies +// or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// Jake Woods +// +// Represents a single file extracted from a multipart/form-data +// stream. +// +// -------------------------------------------------------------------------------------------------------------------- + +using System.IO; +using System.Linq; + +namespace Nancy.HttpMultipart +{ + /// + /// Represents a single file extracted from a multipart/form-data + /// stream. + /// + public class FilePart + { + #region Constructors and Destructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// The name of the input field used for the upload. + /// + /// + /// The name of the file. + /// + /// + /// The file data. + /// + public FilePart(string name, string fileName, Stream data) : + this(name, fileName, data, "text/plain", "form-data") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The name of the input field used for the upload. + /// + /// + /// The name of the file. + /// + /// + /// The file data. + /// + /// + /// The content type. + /// + /// + /// The content disposition. + /// + public FilePart(string name, string fileName, Stream data, string contentType, string contentDisposition) + { + Name = name; + FileName = fileName.Split(Path.GetInvalidFileNameChars()).Last(); + Data = data; + ContentType = contentType; + ContentDisposition = contentDisposition; + } + + #endregion + + #region Public Properties + + /// + /// Gets the data. + /// + public Stream Data { get; private set; } + + /// + /// Gets or sets the file name. + /// + public string FileName { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the content-type. Defaults to text/plain if unspecified. + /// + public string ContentType { get; set; } + + /// + /// Gets or sets the content-disposition. Defaults to form-data if unspecified. + /// + public string ContentDisposition { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/src/Nancy/HttpMultipart/MultipartFormDataParser.cs b/src/Nancy/HttpMultipart/MultipartFormDataParser.cs new file mode 100644 index 0000000000..40015a4c78 --- /dev/null +++ b/src/Nancy/HttpMultipart/MultipartFormDataParser.cs @@ -0,0 +1,285 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2013 Jake Woods +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies +// or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// Jake Woods +// -------------------------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Linq; + +namespace Nancy.HttpMultipart +{ + /// + /// Provides methods to parse a + /// + /// multipart/form-data + /// + /// stream into it's parameters and file data. + /// + /// + /// + /// A parameter is defined as any non-file data passed in the multipart stream. For example + /// any form fields would be considered a parameter. + /// + /// + /// The parser determines if a section is a file or not based on the presence or absence + /// of the filename argument for the Content-Type header. If filename is set then the section + /// is assumed to be a file, otherwise it is assumed to be parameter data. + /// + /// + /// + /// + /// Stream multipartStream = GetTheMultipartStream(); + /// string boundary = GetTheBoundary(); + /// var parser = new MultipartFormDataParser(multipartStream, boundary, Encoding.UTF8); + /// + /// // Grab the parameters (non-file data). Key is based on the name field + /// var username = parser.Parameters["username"].Data; + /// var password = parser.parameters["password"].Data; + /// + /// // Grab the first files data + /// var file = parser.Files.First(); + /// var filename = file.FileName; + /// var filestream = file.Data; + /// + /// + /// // In the context of WCF you can get the boundary from the HTTP + /// // request + /// public ResponseClass MyMethod(Stream multipartData) + /// { + /// // First we need to get the boundary from the header, this is sent + /// // with the HTTP request. We can do that in WCF using the WebOperationConext: + /// var type = WebOperationContext.Current.IncomingRequest.Headers["Content-Type"]; + /// + /// // Now we want to strip the boundary out of the Content-Type, currently the string + /// // looks like: "multipart/form-data; boundary=---------------------124123qase124" + /// var boundary = type.Substring(type.IndexOf('=')+1); + /// + /// // Now that we've got the boundary we can parse our multipart and use it as normal + /// var parser = new MultipartFormDataParser(data, boundary, Encoding.UTF8); + /// + /// ... + /// } + /// + /// + public class MultipartFormDataParser + { + #region Constants + + /// + /// The default buffer size. + /// + private const int DefaultBufferSize = 4096; + + #endregion + + #region Constructors and Destructors + + /// + /// Initializes a new instance of the class + /// with an input stream. Boundary will be automatically detected based on the + /// first line of input. + /// + /// + /// The stream containing the multipart data + /// + public MultipartFormDataParser(Stream stream) + : this(stream, null, Encoding.UTF8, DefaultBufferSize) + { + } + + /// + /// Initializes a new instance of the class + /// with the boundary and input stream. + /// + /// + /// The stream containing the multipart data + /// + /// + /// The multipart/form-data boundary. This should be the value + /// returned by the request header. + /// + public MultipartFormDataParser(Stream stream, string boundary) + : this(stream, boundary, Encoding.UTF8, DefaultBufferSize) + { + } + + /// + /// Initializes a new instance of the class + /// with the input stream and stream encoding. Boundary is automatically + /// detected. + /// + /// + /// The stream containing the multipart data + /// + /// + /// The encoding of the multipart data + /// + public MultipartFormDataParser(Stream stream, Encoding encoding) + : this(stream, null, encoding, DefaultBufferSize) + { + } + + /// + /// Initializes a new instance of the class + /// with the boundary, input stream and stream encoding. + /// + /// + /// The stream containing the multipart data + /// + /// + /// The multipart/form-data boundary. This should be the value + /// returned by the request header. + /// + /// + /// The encoding of the multipart data + /// + public MultipartFormDataParser(Stream stream, string boundary, Encoding encoding) + : this(stream, boundary, encoding, DefaultBufferSize) + { + // 4096 is the optimal buffer size as it matches the internal buffer of a StreamReader + // See: http://stackoverflow.com/a/129318/203133 + // See: http://msdn.microsoft.com/en-us/library/9kstw824.aspx (under remarks) + } + + /// + /// Initializes a new instance of the class + /// with the stream, input encoding and buffer size. Boundary is automatically + /// detected. + /// + /// + /// The stream containing the multipart data + /// + /// + /// The encoding of the multipart data + /// + /// + /// The size of the buffer to use for parsing the multipart form data. This must be larger + /// then (size of boundary + 4 + # bytes in newline). + /// + public MultipartFormDataParser(Stream stream, Encoding encoding, int binaryBufferSize) + : this(stream, null, encoding, binaryBufferSize) + { + } + + /// + /// Initializes a new instance of the class + /// with the boundary, stream, input encoding and buffer size. + /// + /// + /// The stream containing the multipart data + /// + /// + /// The multipart/form-data boundary. This should be the value + /// returned by the request header. + /// + /// + /// The encoding of the multipart data + /// + /// + /// The size of the buffer to use for parsing the multipart form data. This must be larger + /// then (size of boundary + 4 + # bytes in newline). + /// + public MultipartFormDataParser(Stream stream, string boundary, Encoding encoding, int binaryBufferSize) + { + Files = new List(); + Parameters = new List(); + + var streamingParser = new StreamingMultipartFormDataParser(stream, boundary, encoding, binaryBufferSize); + streamingParser.ParameterHandler += parameterPart => Parameters.Add(parameterPart); + + streamingParser.FileHandler += (name, fileName, type, disposition, buffer, bytes) => + { + if (Files.Count == 0 || name != Files[Files.Count - 1].Name) + { + Files.Add(new FilePart(name, fileName, new MemoryStream(), type, disposition)); + } + + Files[Files.Count - 1].Data.Write(buffer, 0, bytes); + }; + + streamingParser.Run(); + + // Reset all the written memory streams so they can be read. + foreach (var file in Files) + { + file.Data.Position = 0; + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the mapping of parameters parsed files. The name of a given field + /// maps to the parsed file data. + /// + public List Files { get; private set; } + + /// + /// Gets the parameters. Several ParameterParts may share the same name. + /// + public List Parameters { get; private set; } + + #endregion + + #region Public Methods + + /// + /// Returns true if the parameter has any values. False otherwise + /// + /// The name of the parameter + /// True if the parameter exists. False otherwise + public bool HasParameter(string name) + { + return Parameters.Any(p => p.Name == name); + } + + /// + /// Returns the value of a parameter or null if it doesn't exist. + /// + /// You should only use this method if you're sure the parameter has only one value. + /// + /// If you need to support multiple values use GetParameterValues. + /// + /// The name of the parameter + /// The value of the parameter + public string GetParameterValue(string name) + { + return Parameters.FirstOrDefault(p => p.Name == name).Data; + } + + /// + /// Returns the values of a parameter or an empty enumerable if the parameter doesn't exist. + /// + /// The name of the parameter + /// The values of the parameter + public IEnumerable GetParameterValues(string name) + { + return Parameters + .Where(p => p.Name == name) + .Select(p => p.Data); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Nancy/HttpMultipart/MultipartParseException.cs b/src/Nancy/HttpMultipart/MultipartParseException.cs new file mode 100644 index 0000000000..3b1d5ef0ba --- /dev/null +++ b/src/Nancy/HttpMultipart/MultipartParseException.cs @@ -0,0 +1,51 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2013 Jake Woods +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies +// or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// Jake Woods +// +// Represents a parsing problem occurring within the MultipartFormDataParser +// +// -------------------------------------------------------------------------------------------------------------------- + +using System; + +namespace Nancy.HttpMultipart +{ + /// + /// Represents a parsing problem occurring within the MultipartFormDataParser + /// + [Serializable] + internal class MultipartParseException : Exception + { + #region Constructors and Destructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// The message. + /// + public MultipartParseException(string message) + : base(message) + { + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Nancy/HttpMultipart/MultipartStreamPart.cs b/src/Nancy/HttpMultipart/MultipartStreamPart.cs new file mode 100644 index 0000000000..fb14b8f379 --- /dev/null +++ b/src/Nancy/HttpMultipart/MultipartStreamPart.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Nancy.HttpMultipart +{ + internal class MultipartStreamPart + { + } +} diff --git a/src/Nancy/HttpMultipart/ParameterPart.cs b/src/Nancy/HttpMultipart/ParameterPart.cs new file mode 100644 index 0000000000..3ea1a71b53 --- /dev/null +++ b/src/Nancy/HttpMultipart/ParameterPart.cs @@ -0,0 +1,72 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2013 Jake Woods +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies +// or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// Jake Woods +// +// Represents a single parameter extracted from a multipart/form-data +// stream. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Nancy.HttpMultipart +{ + /// + /// Represents a single parameter extracted from a multipart/form-data + /// stream. + /// + /// + /// For our purposes a "parameter" is defined as any non-file data + /// in the multipart/form-data stream. + /// + public class ParameterPart + { + #region Constructors and Destructors + + /// + /// Initializes a new instance of the class. + /// + /// + /// The name. + /// + /// + /// The data. + /// + public ParameterPart(string name, string data) + { + Name = name; + Data = data; + } + + #endregion + + #region Public Properties + + /// + /// Gets the data. + /// + public string Data { get; private set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } + + #endregion + } +} \ No newline at end of file diff --git a/src/Nancy/HttpMultipart/RebufferableBinaryReader.cs b/src/Nancy/HttpMultipart/RebufferableBinaryReader.cs new file mode 100644 index 0000000000..fa9d19c3d9 --- /dev/null +++ b/src/Nancy/HttpMultipart/RebufferableBinaryReader.cs @@ -0,0 +1,349 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2013 Jake Woods +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies +// or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// Jake Woods +// +// Provides methods to interpret and read a stream as either character or binary +// data similar to a and provides the ability to push +// data onto the front of the stream. +// +// -------------------------------------------------------------------------------------------------------------------- + +using System; +using System.IO; +using System.Text; + +namespace Nancy.HttpMultipart +{ + /// + /// Provides methods to interpret and read a stream as either character or binary + /// data similar to a and provides the ability to push + /// data onto the front of the stream. + /// + internal class RebufferableBinaryReader + { + #region Fields + + /// + /// The size of the buffer to use when reading new data. + /// + private readonly int bufferSize; + + /// + /// The encoding to use for character based operations + /// + private readonly Encoding encoding; + + /// + /// The stream to read raw data from. + /// + private readonly Stream stream; + + /// + /// The stream stack to store buffered data. + /// + private readonly BinaryStreamStack streamStack; + + #endregion + + #region Constructors and Destructors + + /// + /// Initializes a new instance of the class. + /// Default encoding of UTF8 will be used. + /// + /// + /// The input stream to read from. + /// + public RebufferableBinaryReader(Stream input) + : this(input, new UTF8Encoding(false)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The input stream to read from. + /// + /// + /// The encoding to use for character based operations. + /// + public RebufferableBinaryReader(Stream input, Encoding encoding) + : this(input, encoding, 4096) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The input stream to read from. + /// + /// + /// The encoding to use for character based operations. + /// + /// + /// The buffer size to use for new buffers. + /// + public RebufferableBinaryReader(Stream input, Encoding encoding, int bufferSize) + { + stream = input; + streamStack = new BinaryStreamStack(encoding); + this.encoding = encoding; + this.bufferSize = bufferSize; + } + + #endregion + + #region Public Methods and Operators + + /// + /// Adds data to the front of the stream. The most recently buffered data will + /// be read first. + /// + /// + /// The data to buffer. + /// + public void Buffer(byte[] data) + { + streamStack.Push(data); + } + + /// + /// Adds the string to the front of the stream. The most recently buffered data will + /// be read first. + /// + /// + /// The data. + /// + public void Buffer(string data) + { + streamStack.Push(encoding.GetBytes(data)); + } + + /// + /// Reads a single byte as an integer from the stream. Returns -1 if no + /// data is left to read. + /// + /// + /// The that was read. + /// + public int Read() + { + int value = -1; + while (value == -1) + { + if (!streamStack.HasData()) + { + if (StreamData() == 0) + { + return -1; + } + } + + value = streamStack.Read(); + } + + return value; + } + + /// + /// Reads the specified number of bytes from the stream, starting from a + /// specified point in the byte array. + /// + /// + /// The buffer to read data into. + /// + /// + /// The index of buffer to start reading into. + /// + /// + /// The number of bytes to read into the buffer. + /// + /// + /// The number of bytes read into buffer. This might be less than the number of bytes requested if that many bytes are not available, + /// or it might be zero if the end of the stream is reached. + /// + public int Read(byte[] buffer, int index, int count) + { + int amountRead = 0; + while (amountRead < count) + { + if (!streamStack.HasData()) + { + if (StreamData() == 0) + { + return amountRead; + } + } + + amountRead += streamStack.Read(buffer, index + amountRead, count - amountRead); + } + + return amountRead; + } + + /// + /// Reads the specified number of characters from the stream, starting from a + /// specified point in the byte array. + /// + /// + /// The buffer to read data into. + /// + /// + /// The index of buffer to start reading into. + /// + /// + /// The number of characters to read into the buffer. + /// + /// + /// The number of characters read into buffer. This might be less than the number of + /// characters requested if that many characters are not available, + /// or it might be zero if the end of the stream is reached. + /// + public int Read(char[] buffer, int index, int count) + { + int amountRead = 0; + while (amountRead < count) + { + if (!streamStack.HasData()) + { + if (StreamData() == 0) + { + return amountRead; + } + } + + amountRead += streamStack.Read(buffer, index + amountRead, count - amountRead); + } + + return amountRead; + } + + /// + /// Reads a series of bytes delimited by the byte encoding of newline for this platform. + /// the newline bytes will not be included in the return data. + /// + /// + /// A byte array containing all the data up to but not including the next newline in the stack. + /// + public byte[] ReadByteLine() + { + var builder = new MemoryStream(); + while (true) + { + if (!streamStack.HasData()) + { + if (StreamData() == 0) + { + return builder.Length > 0 ? builder.ToArray() : null; + } + } + + bool hitStreamEnd; + byte[] line = streamStack.ReadByteLine(out hitStreamEnd); + + builder.Write(line, 0, line.Length); + if (!hitStreamEnd) + { + return builder.ToArray(); + } + } + } + + /// + /// Reads a line from the stack delimited by the newline for this platform. The newline + /// characters will not be included in the stream + /// + /// + /// The containing the line or null if end of stream. + /// + public string ReadLine() + { + byte[] data = ReadByteLine(); + return data == null ? null : encoding.GetString(data); + } + + #endregion + + #region Methods + + /// + /// Determines the byte order marking offset (if any) from the + /// given buffer. + /// + /// + /// The buffer to examine. + /// + /// + /// The representing the length of the byte order marking. + /// + private int GetBomOffset(byte[] buffer) + { + byte[] bom = encoding.GetPreamble(); + bool usesBom = true; + for (int i = 0; i < bom.Length; ++i) + { + if (bom[i] != buffer[i]) + { + usesBom = false; + } + } + + return usesBom ? bom.Length : 0; + } + + /// + /// Reads more data from the stream into the stream stack. + /// + /// + /// The number of bytes read into the stream stack as an + /// + private int StreamData() + { + var buffer = new byte[bufferSize]; + int amountRead = stream.Read(buffer, 0, buffer.Length); + + // We need to check if our stream is using our encodings + // BOM, if it is we need to jump it. + int bomOffset = GetBomOffset(buffer); + + // Sometimes we'll get a buffer that's smaller then we expect, chop it down + // for the reader: + if (amountRead - bomOffset > 0) + { + if (amountRead != buffer.Length || bomOffset > 0) + { + var smallBuffer = new byte[amountRead - bomOffset]; + System.Buffer.BlockCopy(buffer, bomOffset, smallBuffer, 0, amountRead - bomOffset); + streamStack.Push(smallBuffer); + } + else + { + streamStack.Push(buffer); + } + } + + return amountRead; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Nancy/HttpMultipart/StreamingMultipartFormDataParser.cs b/src/Nancy/HttpMultipart/StreamingMultipartFormDataParser.cs new file mode 100644 index 0000000000..ceb2b410ce --- /dev/null +++ b/src/Nancy/HttpMultipart/StreamingMultipartFormDataParser.cs @@ -0,0 +1,752 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Nancy.HttpMultipart +{ + /// + /// Provides methods to parse a + /// + /// multipart/form-data + /// + /// stream into it's parameters and file data. + /// + /// + /// + /// A parameter is defined as any non-file data passed in the multipart stream. For example + /// any form fields would be considered a parameter. + /// + /// + /// The parser determines if a section is a file or not based on the presence or absence + /// of the filename argument for the Content-Type header. If filename is set then the section + /// is assumed to be a file, otherwise it is assumed to be parameter data. + /// + /// + /// + /// + /// Stream multipartStream = GetTheMultipartStream(); + /// string boundary = GetTheBoundary(); + /// var parser = new StreamingMultipartFormDataParser(multipartStream, boundary, Encoding.UTF8); + /// + /// // Set up our delegates for how we want to handle recieved data. + /// // In our case parameters will be written to a dictionary and files + /// // will be written to a filestream + /// parser.ParameterHandler += parameter => AddToDictionary(parameter); + /// parser.FileHandler += (name, fileName, type, disposition, buffer, bytes) => WriteDataToFile(fileName, buffer, bytes); + /// parser.Run(); + /// + /// + public class StreamingMultipartFormDataParser + { + #region Constants + + /// + /// The default buffer size. + /// + private const int DefaultBufferSize = 4 * 1024 * 1024; + + #endregion + + #region Fields + + /// + /// The stream we are parsing. + /// + private readonly Stream stream; + + /// + /// The boundary of the multipart message as a string. + /// + private string boundary; + + /// + /// The boundary of the multipart message as a byte string + /// encoded with CurrentEncoding + /// + private byte[] boundaryBinary; + + /// + /// The end boundary of the multipart message as a string. + /// + private string endBoundary; + + /// + /// The end boundary of the multipart message as a byte string + /// encoded with CurrentEncoding + /// + private byte[] endBoundaryBinary; + + /// + /// Determines if we have consumed the end boundary binary and determines + /// if we are done parsing. + /// + private bool readEndBoundary; + + #endregion + + #region Constructors and Destructors + + /// + /// Initializes a new instance of the class + /// with an input stream. Boundary will be automatically detected based on the + /// first line of input. + /// + /// + /// The stream containing the multipart data + /// + public StreamingMultipartFormDataParser(Stream stream) + : this(stream, null, Encoding.UTF8, DefaultBufferSize) + { + } + + /// + /// Initializes a new instance of the class + /// with the boundary and input stream. + /// + /// + /// The stream containing the multipart data + /// + /// + /// The multipart/form-data boundary. This should be the value + /// returned by the request header. + /// + public StreamingMultipartFormDataParser(Stream stream, string boundary) + : this(stream, boundary, Encoding.UTF8, DefaultBufferSize) + { + } + + /// + /// Initializes a new instance of the class + /// with the input stream and stream encoding. Boundary is automatically + /// detected. + /// + /// + /// The stream containing the multipart data + /// + /// + /// The encoding of the multipart data + /// + public StreamingMultipartFormDataParser(Stream stream, Encoding encoding) + : this(stream, null, encoding, DefaultBufferSize) + { + } + + /// + /// Initializes a new instance of the class + /// with the boundary, input stream and stream encoding. + /// + /// + /// The stream containing the multipart data + /// + /// + /// The multipart/form-data boundary. This should be the value + /// returned by the request header. + /// + /// + /// The encoding of the multipart data + /// + public StreamingMultipartFormDataParser(Stream stream, string boundary, Encoding encoding) + : this(stream, boundary, encoding, DefaultBufferSize) + { + // 4096 is the optimal buffer size as it matches the internal buffer of a StreamReader + // See: http://stackoverflow.com/a/129318/203133 + // See: http://msdn.microsoft.com/en-us/library/9kstw824.aspx (under remarks) + } + + /// + /// Initializes a new instance of the class + /// with the stream, input encoding and buffer size. Boundary is automatically + /// detected. + /// + /// + /// The stream containing the multipart data + /// + /// + /// The encoding of the multipart data + /// + /// + /// The size of the buffer to use for parsing the multipart form data. This must be larger + /// then (size of boundary + 4 + # bytes in newline). + /// + public StreamingMultipartFormDataParser(Stream stream, Encoding encoding, int binaryBufferSize) + : this(stream, null, encoding, binaryBufferSize) + { + } + + /// + /// Initializes a new instance of the class + /// with the boundary, stream, input encoding and buffer size. + /// + /// + /// The stream containing the multipart data + /// + /// + /// The multipart/form-data boundary. This should be the value + /// returned by the request header. + /// + /// + /// The encoding of the multipart data + /// + /// + /// The size of the buffer to use for parsing the multipart form data. This must be larger + /// then (size of boundary + 4 + # bytes in newline). + /// + public StreamingMultipartFormDataParser(Stream stream, string boundary, Encoding encoding, int binaryBufferSize) + { + if(stream == null || stream == Stream.Null) { throw new ArgumentNullException("stream"); } + if(encoding == null) { throw new ArgumentNullException("encoding"); } + + this.stream = stream; + this.boundary = boundary; + Encoding = encoding; + BinaryBufferSize = binaryBufferSize; + readEndBoundary = false; + } + + #endregion + + /// + /// Begins executing the parser. This should be called after all handlers have been set. + /// + public void Run() + { + var reader = new RebufferableBinaryReader(stream, Encoding, BinaryBufferSize); + + // If we don't know the boundary now is the time to calculate it. + if (boundary == null) + { + boundary = DetectBoundary(reader); + } + + // It's important to remember that the boundary given in the header has a -- appended to the start + // and the last one has a -- appended to the end + boundary = "--" + boundary; + endBoundary = boundary + "--"; + + // We add newline here because unlike reader.ReadLine() binary reading + // does not automatically consume the newline, we want to add it to our signature + // so we can automatically detect and consume newlines after the boundary + boundaryBinary = Encoding.GetBytes(boundary); + endBoundaryBinary = Encoding.GetBytes(endBoundary); + + Debug.Assert( + BinaryBufferSize >= endBoundaryBinary.Length, + "binaryBufferSize must be bigger then the boundary"); + + Parse(reader); + } + + #region Public Properties + + /// + /// The FileStreamDelegate defining functions that can handle file stream data from this parser. + /// + /// Delegates can assume that the data is sequential i.e. the data recieved by any delegates will be + /// the data immediately following any previously recieved data. + /// + /// The name of the multipart data + /// The name of the file + /// The content type of the multipart data + /// The content disposition of the multipart data + /// Some of the data from the file (not neccecarily all of the data) + /// The length of data in buffer + public delegate void FileStreamDelegate( + string name, string fileName, string contentType, string contentDisposition, byte[] buffer, int bytes); + + public delegate void StreamClosedDelegate(); + + /// + /// The ParameterDelegate defining functions that can handle multipart parameter data + /// + /// The parsed parameter part + public delegate void ParameterDelegate(ParameterPart part); + + /// + /// Gets or sets the binary buffer size. + /// + public static int BinaryBufferSize { get; set; } + + /// + /// Gets the encoding. + /// + public Encoding Encoding { get; private set; } + + /// + /// The FileHandler. Delegates attached to this property will recieve sequential file stream data from this parser. + /// + public FileStreamDelegate FileHandler { get; set; } + + /// + /// The ParameterHandler. Delegates attached to this property will recieve parameter data. + /// + public ParameterDelegate ParameterHandler { get; set; } + + /// + /// The StreamClosedHandler. Delegates attached to this property will be notified when the source stream is exhausted. + /// + public StreamClosedDelegate StreamClosedHandler { get; set; } + + #endregion + + #region Methods + + /// + /// Detects the boundary from the input stream. Assumes that the + /// current position of the reader is the start of the file and therefore + /// the beginning of the boundary. + /// + /// + /// The binary reader to parse + /// + /// + /// The boundary string + /// + private static string DetectBoundary(RebufferableBinaryReader reader) + { + // Presumably the boundary is --|||||||||||||| where -- is the stuff added on to + // the front as per the protocol and ||||||||||||| is the part we care about. + string boundary = string.Concat(reader.ReadLine().Skip(2)); + reader.Buffer("--" + boundary + "\n"); + return boundary; + } + + /// + /// Finds the next sequence of newlines in the input stream. + /// + /// The data to search + /// The offset to start searching at + /// The maximum number of bytes (starting from offset) to search. + /// The offset of the next newline + private int FindNextNewline(ref byte[] data, int offset, int maxBytes) + { + byte[][] newlinePatterns = {Encoding.GetBytes("\r\n"), Encoding.GetBytes("\n")}; + Array.Sort(newlinePatterns, (first, second) => second.Length.CompareTo(first.Length)); + + byte[] dataRef = data; + if (offset != 0) + { + dataRef = data.Skip(offset).ToArray(); + } + + foreach (var pattern in newlinePatterns) + { + int position = SubsequenceFinder.Search(dataRef, pattern, maxBytes); + if (position != -1) + { + return position + offset; + } + } + + return -1; + } + + /// + /// Calculates the length of the next found newline. + /// data[offset] is the start of the space to search. + /// + /// + /// The data containing the newline + /// + /// + /// The offset of the start of the newline + /// + /// + /// The length in bytes of the newline sequence + /// + private int CalculateNewlineLength(ref byte[] data, int offset) + { + byte[][] newlinePatterns = {Encoding.GetBytes("\r\n"), Encoding.GetBytes("\n")}; + + // Go through each pattern and find which one matches. + foreach (var pattern in newlinePatterns) + { + bool found = false; + for (int i = 0; i < pattern.Length; ++i) + { + if (pattern[i] != data[offset + i]) + { + found = false; + break; + } + + found = true; + } + + if (found) + { + return pattern.Length; + } + } + + return 0; + } + + /// + /// Begins the parsing of the stream into objects. + /// + /// + /// The multipart/form-data binary reader to parse from. + /// + /// + /// thrown on finding unexpected data such as a boundary before we are ready for one. + /// + private void Parse(RebufferableBinaryReader reader) + { + // Parsing references include: + // RFC1341 section 7: http://www.w3.org/Protocols/rfc1341/7_2_Multipart.html + // RFC2388: http://www.ietf.org/rfc/rfc2388.txt + + // First we need to read untill we find a boundary + while (true) + { + string line = reader.ReadLine(); + if (line == boundary) + { + break; + } + + if (line == null) + { + throw new MultipartParseException("Could not find expected boundary"); + } + } + + // Now that we've found the initial boundary we know where to start. + // We need parse each individual section + while (!readEndBoundary) + { + // ParseSection will parse up to and including + // the next boundary. + ParseSection(reader); + } + + if (StreamClosedHandler != null) + { + StreamClosedHandler(); + } + } + + /// + /// Parses a section of the stream that is known to be file data. + /// + /// + /// The header parameters of this file, expects "name" and "filename" to be valid keys + /// + /// + /// The StreamReader to read the data from + /// + /// + /// The con[] taining the parsed data (name, filename, stream containing file). + /// + private void ParseFilePart(Dictionary parameters, RebufferableBinaryReader reader) + { + string name = parameters["name"]; + string filename = parameters["filename"]; + string contentType = parameters.ContainsKey("content-type") ? parameters["content-type"] : "text/plain"; + string contentDisposition = parameters.ContainsKey("content-disposition") + ? parameters["content-disposition"] + : "form-data"; + + // We want to create a stream and fill it with the data from the + // file. + var curBuffer = new byte[BinaryBufferSize]; + var prevBuffer = new byte[BinaryBufferSize]; + var fullBuffer = new byte[BinaryBufferSize*2]; + int curLength = 0; + int prevLength = 0; + int fullLength = 0; + + prevLength = reader.Read(prevBuffer, 0, prevBuffer.Length); + do + { + curLength = reader.Read(curBuffer, 0, curBuffer.Length); + + // Combine both buffers into the fullBuffer + // See: http://stackoverflow.com/questions/415291/best-way-to-combine-two-or-more-byte-arrays-in-c-sharp + Buffer.BlockCopy(prevBuffer, 0, fullBuffer, 0, prevLength); + Buffer.BlockCopy(curBuffer, 0, fullBuffer, prevLength, curLength); + fullLength = prevLength + curLength; + + // Now we want to check for a substring within the current buffer. + // We need to find the closest substring greedily. That is find the + // closest boundary and don't miss the end --'s if it's an end boundary. + int endBoundaryPos = SubsequenceFinder.Search(fullBuffer, endBoundaryBinary, fullLength); + int endBoundaryLength = endBoundaryBinary.Length; + + int boundaryPos = SubsequenceFinder.Search(fullBuffer, boundaryBinary, fullLength); + int boundaryLength = boundaryBinary.Length; + + // If the boundaryPos is exactly at the end of our full buffer then ignore it as it could + // actually be a endBoundary that's had the '--' chopped off by the buffer. + if(boundaryPos + boundaryLength == fullLength) + { + boundaryPos = -1; + } + + // We need to select the appropriate position and length + // based on the smallest non-negative position. + int endPos = -1; + int endPosLength = 0; + + if (endBoundaryPos >= 0 && boundaryPos >= 0) + { + if (boundaryPos < endBoundaryPos) + { + // Select boundary + endPos = boundaryPos; + endPosLength = boundaryLength; + } + else + { + // Select end boundary + endPos = endBoundaryPos; + endPosLength = endBoundaryLength; + readEndBoundary = true; + } + } + else if (boundaryPos >= 0 && endBoundaryPos < 0) + { + // Select boundary + endPos = boundaryPos; + endPosLength = boundaryLength; + } + else if (boundaryPos < 0 && endBoundaryPos >= 0) + { + // Select end boundary + endPos = endBoundaryPos; + endPosLength = endBoundaryLength; + readEndBoundary = true; + } + + if (endPos != -1) + { + // Now we need to check if the endPos is followed by \r\n or just \n. HTTP + // specifies \r\n but some clients might encode with \n. Or we might get 0 if + // we are at the end of the file. + int boundaryNewlineOffset = CalculateNewlineLength(ref fullBuffer, + Math.Min(fullLength - 1, + endPos + endPosLength)); + + // We also need to check if the last n characters of the buffer to write + // are a newline and if they are ignore them. + int maxNewlineBytes = Encoding.GetMaxByteCount(2); + int bufferNewlineOffset = FindNextNewline( + ref fullBuffer, Math.Max(0, endPos - maxNewlineBytes), maxNewlineBytes); + int bufferNewlineLength = CalculateNewlineLength(ref fullBuffer, bufferNewlineOffset); + + // We've found an end. We need to consume all the binary up to it + // and then write the remainder back to the original stream. Then we + // need to modify the original streams position to take into account + // the new data. + // We also want to chop off the newline that is inserted by the protocl. + // We can do this by reducing endPos by the length of newline in this environment + // and encoding + FileHandler(name, filename, contentType, contentDisposition, fullBuffer, + endPos - bufferNewlineLength); + + int writeBackOffset = endPos + endPosLength + boundaryNewlineOffset; + int writeBackAmount = (prevLength + curLength) - writeBackOffset; + var writeBackBuffer = new byte[writeBackAmount]; + Buffer.BlockCopy(fullBuffer, writeBackOffset, writeBackBuffer, 0, writeBackAmount); + reader.Buffer(writeBackBuffer); + + break; + } + + // No end, consume the entire previous buffer + FileHandler(name, filename, contentType, contentDisposition, prevBuffer, prevLength); + + // Now we want to swap the two buffers, we don't care + // what happens to the data from prevBuffer so we set + // curBuffer to it so it gets overwrited. + byte[] tempBuffer = curBuffer; + curBuffer = prevBuffer; + prevBuffer = tempBuffer; + + // We don't need to swap the lengths because + // curLength will be overwritten in the next + // iteration of the loop. + prevLength = curLength; + } while (prevLength != 0); + } + + /// + /// Parses a section of the stream that is known to be parameter data. + /// + /// + /// The header parameters of this section. "name" must be a valid key. + /// + /// + /// The StreamReader to read the data from + /// + /// + /// The containing the parsed data (name, value). + /// + /// + /// thrown if unexpected data is found such as running out of stream before hitting the boundary. + /// + private void ParseParameterPart(Dictionary parameters, RebufferableBinaryReader reader) + { + // Our job is to get the actual "data" part of the parameter and construct + // an actual ParameterPart object with it. All we need to do is read data into a string + // untill we hit the boundary + var data = new StringBuilder(); + bool firstTime = true; + string line = reader.ReadLine(); + while (line != boundary && line != endBoundary) + { + if (line == null) + { + throw new MultipartParseException("Unexpected end of stream. Is there an end boundary?"); + } + + if (firstTime) + { + data.Append(line); + firstTime = false; + } + else + { + data.Append(Environment.NewLine); + data.Append(line); + } + line = reader.ReadLine(); + } + + if (line == endBoundary) + { + readEndBoundary = true; + } + + // If we're here we've hit the boundary and have the data! + var part = new ParameterPart(parameters["name"], data.ToString()); + ParameterHandler(part); + } + + /// + /// Parses the header of the next section of the multipart stream and + /// determines if it contains file data or parameter data. + /// + /// + /// The StreamReader to read data from. + /// + /// + /// thrown if unexpected data is hit such as end of stream. + /// + private void ParseSection(RebufferableBinaryReader reader) + { + // Our first job is to determine what type of section this is: form data or file. + // This is a bit tricky because files can still be encoded with Content-Disposition: form-data + // in the case of single file uploads. Multi-file uploads have Content-Disposition: file according + // to the spec however in practise it seems that multiple files will be represented by + // multiple Content-Disposition: form-data files. + var parameters = new Dictionary(); + + string line = reader.ReadLine(); + while (line != string.Empty) + { + if (line == null) + { + throw new MultipartParseException("Unexpected end of stream"); + } + + if (line == boundary || line == endBoundary) + { + throw new MultipartParseException("Unexpected end of section"); + } + + + // This line parses the header values into a set of key/value pairs. For example: + // Content-Disposition: form-data; name="textdata" + // ["content-disposition"] = "form-data" + // ["name"] = "textdata" + // Content-Disposition: form-data; name="file"; filename="data.txt" + // ["content-disposition"] = "form-data" + // ["name"] = "file" + // ["filename"] = "data.txt" + // Content-Type: text/plain + // ["content-type"] = "text/plain" + Dictionary values = SplitBySemicolonIgnoringSemicolonsInQuotes(line) + .Select(x => x.Split(new[] {':', '='}, 2)) + // select where the length of the array is equal to two, that way if it is only one it will + // be ignored as it is invalid key-pair + .Where(x=> x.Length == 2) + // Limit split to 2 splits so we don't accidently split characters in file paths. + .ToDictionary( + x => x[0].Trim().Replace("\"", string.Empty).ToLower(), + x => x[1].Trim().Replace("\"", string.Empty)); + + + // Here we just want to push all the values that we just retrieved into the + // parameters dictionary. + try + { + foreach (var pair in values) + { + parameters.Add(pair.Key, pair.Value); + } + } + catch (ArgumentException) + { + throw new MultipartParseException("Duplicate field in section"); + } + + line = reader.ReadLine(); + } + + // Now that we've consumed all the parameters we're up to the body. We're going to do + // different things depending on if we're parsing a, relatively small, form value or a + // potentially large file. + if (parameters.ContainsKey("filename")) + { + // Right now we assume that if a section contains filename then it is a file. + // This assumption needs to be checked, it holds true in firefox but is untested for other + // browsers. + ParseFilePart(parameters, reader); + } + else + { + ParseParameterPart(parameters, reader); + } + } + + /// + /// Splits a line by semicolons but ignores semicolons in quotes. + /// + /// The line to split + /// The split strings + private IEnumerable SplitBySemicolonIgnoringSemicolonsInQuotes(string line) + { + // Loop over the line looking for a semicolon. Keep track of if we're currently inside quotes + // and if we are don't treat a semicolon as a splitting character. + bool inQuotes = false; + string workingString = ""; + + foreach (char c in line) + { + if (c == '"') + { + inQuotes = !inQuotes; + } + + if (c == ';' && !inQuotes) + { + yield return workingString; + workingString = ""; + } + else + { + workingString += c; + } + } + + yield return workingString; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Nancy/HttpMultipart/SubsequenceFinder.cs b/src/Nancy/HttpMultipart/SubsequenceFinder.cs new file mode 100644 index 0000000000..cddf826950 --- /dev/null +++ b/src/Nancy/HttpMultipart/SubsequenceFinder.cs @@ -0,0 +1,97 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) 2013 Jake Woods +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software +// and associated documentation files (the "Software"), to deal in the Software without restriction, +// including without limitation the rights to use, copy, modify, merge, publish, distribute, +// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies +// or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// Jake Woods +// +// Provides methods to find a subsequence within a +// sequence. +// +// -------------------------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; + +namespace Nancy.HttpMultipart +{ + /// + /// Provides methods to find a subsequence within a + /// sequence. + /// + internal class SubsequenceFinder + { + #region Public Methods and Operators + + public static int Search(byte[] haystack, byte[] needle) + { + return Search(haystack, needle, haystack.Length); + } + + /// + /// Finds if a sequence exists within another sequence. + /// + /// + /// The sequence to search + /// + /// + /// The sequence to look for + /// + /// + /// The length of the haystack to consider for searching + /// + /// + /// The start position of the found sequence or -1 if nothing was found + /// + public static int Search(byte[] haystack, byte[] needle, int haystackLength) + { + var charactersInNeedle = new HashSet(needle); + + var length = needle.Length; + var index = 0; + while (index + length <= haystackLength) + { + // Worst case scenario: Go back to character-by-character parsing until we find a non-match + // or we find the needle. + if (charactersInNeedle.Contains(haystack[index + length - 1])) + { + var needleIndex = 0; + while (haystack[index + needleIndex] == needle[needleIndex]) + { + if (needleIndex == needle.Length - 1) + { + // Found our match! + return index; + } + + needleIndex += 1; + } + + index += 1; + index += needleIndex; + continue; + } + + index += length; + } + + return -1; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Nancy/Request.cs b/src/Nancy/Request.cs index 32eb004251..be480a8817 100644 --- a/src/Nancy/Request.cs +++ b/src/Nancy/Request.cs @@ -1,3 +1,5 @@ +using Nancy.HttpMultipart; + namespace Nancy { using System; @@ -230,6 +232,9 @@ public dynamic Form public void Dispose() { ((IDisposable)this.Body).Dispose(); + foreach (var item in this.Files) { + item.Value.Close (); + } } private void ParseFormData() @@ -253,26 +258,26 @@ private void ParseFormData() return; } - var boundary = Regex.Match(contentType, @"boundary=""?(?[^\n\;\"" ]*)").Groups["token"].Value; - var multipart = new HttpMultipart(this.Body, boundary); - - var formValues = + var formValues = new NameValueCollection(StaticConfiguration.CaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase); - foreach (var httpMultipartBoundary in multipart.GetBoundaries()) - { - if (string.IsNullOrEmpty(httpMultipartBoundary.Filename)) - { - var reader = - new StreamReader(httpMultipartBoundary.Value); - formValues.Add(httpMultipartBoundary.Name, reader.ReadToEnd()); + var filestreamsByName = new Dictionary (); - } - else + var parser = new StreamingMultipartFormDataParser(this.Body); + parser.FileHandler += (string name, string fileName, string _contentType, string contentDisposition, byte[] buffer, int bytes) => { + if (!filestreamsByName.ContainsKey(fileName)) { - this.files.Add(new HttpFile(httpMultipartBoundary)); + string f = System.IO.Path.GetTempFileName(); + var fs = System.IO.File.Create (f, bytes, System.IO.FileOptions.DeleteOnClose); + filestreamsByName.Add(fileName, fs); + this.files.Add(new HttpFile(contentType, fileName, fs, name)); } - } + filestreamsByName[fileName].Write(buffer, 0, bytes); + }; + parser.ParameterHandler += (ParameterPart part) => { + formValues.Add(part.Name, part.Data); + }; + parser.Run(); foreach (var key in formValues.AllKeys.Where(key => key != null)) {