实例介绍
【实例简介】http 解析
BinaryStreamStack.cs
FilePart.csMultipartParseException.cs
MultipartParser.cs
ParameterPart.cs
RebufferableBinaryReader.cs
SubsequenceFinder.cs
【实例截图】
【核心代码】
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MultipartFormDataParser.cs" company="Jake Woods">
// 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,
// sub-license, 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.
// </copyright>
// <author>Jake Woods</author>
// --------------------------------------------------------------------------------------------------------------------
namespace Wenhe.Foundation.Web.HttpMultipart
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
/// <summary>
/// Provides methods to parse a
/// <see href="http://www.ietf.org/rfc/rfc2388.txt">
/// <c>multipart/form-data</c>
/// </see>
/// stream into it's parameters and file data.
/// </summary>
/// <remarks>
/// <para>
/// A parameter is defined as any non-file data passed in the multipart stream. For example
/// any form fields would be considered a parameter.
/// </para>
/// <para>
/// 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.
/// </para>
/// </remarks>
/// <example>
/// <code lang="C#">
/// 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;
/// </code>
/// <code lang="C#">
/// // 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);
///
/// ...
/// }
/// </code>
/// </example>
public class MultipartParser
{
#region Constants
/// <summary>
/// The default buffer size.
/// </summary>
private const int DefaultBufferSize = 4096;
#endregion
#region Fields
/// <summary>
/// The boundary of the multipart message as a string.
/// </summary>
private readonly string boundary;
/// <summary>
/// The boundary of the multipart message as a byte string
/// encoded with CurrentEncoding
/// </summary>
private readonly byte[] boundaryBinary;
/// <summary>
/// The end boundary of the multipart message as a string.
/// </summary>
private readonly string endBoundary;
/// <summary>
/// The end boundary of the multipart message as a byte string
/// encoded with CurrentEncoding
/// </summary>
private readonly byte[] endBoundaryBinary;
/// <summary>
/// Determines if we have consumed the end boundary binary and determines
/// if we are done parsing.
/// </summary>
private bool readEndBoundary;
#endregion
#region Constructors and Destructors
/// <summary>
/// Initializes a new instance of the <see cref="MultipartParser"/> class
/// with an input stream. Boundary will be automatically detected based on the
/// first line of input.
/// </summary>
/// <param name="stream">
/// The stream containing the multipart data
/// </param>
public MultipartParser(Stream stream)
: this(stream, null, Encoding.UTF8, DefaultBufferSize)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MultipartParser"/> class
/// with the boundary and input stream.
/// </summary>
/// <param name="stream">
/// The stream containing the multipart data
/// </param>
/// <param name="boundary">
/// The multipart/form-data boundary. This should be the value
/// returned by the request header.
/// </param>
public MultipartParser(Stream stream, string boundary)
: this(stream, boundary, Encoding.UTF8, DefaultBufferSize)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MultipartParser"/> class
/// with the input stream and stream encoding. Boundary is automatically
/// detected.
/// </summary>
/// <param name="stream">
/// The stream containing the multipart data
/// </param>
/// <param name="encoding">
/// The encoding of the multipart data
/// </param>
public MultipartParser(Stream stream, Encoding encoding)
: this(stream, null, encoding, DefaultBufferSize)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MultipartParser"/> class
/// with the boundary, input stream and stream encoding.
/// </summary>
/// <param name="stream">
/// The stream containing the multipart data
/// </param>
/// <param name="boundary">
/// The multipart/form-data boundary. This should be the value
/// returned by the request header.
/// </param>
/// <param name="encoding">
/// The encoding of the multipart data
/// </param>
public MultipartParser(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)
}
/// <summary>
/// Initializes a new instance of the <see cref="MultipartParser"/> class
/// with the stream, input encoding and buffer size. Boundary is automatically
/// detected.
/// </summary>
/// <param name="stream">
/// The stream containing the multipart data
/// </param>
/// <param name="encoding">
/// The encoding of the multipart data
/// </param>
/// <param name="binaryBufferSize">
/// 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).
/// </param>
public MultipartParser(Stream stream, Encoding encoding, int binaryBufferSize)
: this(stream, null, encoding, binaryBufferSize)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MultipartParser"/> class
/// with the boundary, stream, input encoding and buffer size.
/// </summary>
/// <param name="stream">
/// The stream containing the multipart data
/// </param>
/// <param name="boundary">
/// The multipart/form-data boundary. This should be the value
/// returned by the request header.
/// </param>
/// <param name="encoding">
/// The encoding of the multipart data
/// </param>
/// <param name="binaryBufferSize">
/// 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).
/// </param>
public MultipartParser(Stream stream, string boundary, Encoding encoding, int binaryBufferSize)
{
this.Parameters = new Dictionary<string, ParameterPart>();
this.Files = new List<FilePart>();
this.Encoding = encoding;
this.BinaryBufferSize = binaryBufferSize;
this.readEndBoundary = false;
using (var reader = new RebufferableBinaryReader(stream, this.Encoding, this.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
this.boundary = "--" boundary;
this.endBoundary = this.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
this.boundaryBinary = this.Encoding.GetBytes(this.boundary);
this.endBoundaryBinary = this.Encoding.GetBytes(this.endBoundary);
Debug.Assert(
binaryBufferSize >= this.endBoundaryBinary.Length,
"binaryBufferSize must be bigger then the boundary");
this.Parse(reader);
}
}
#endregion
#region Public Properties
/// <summary>
/// Gets or sets the binary buffer size.
/// </summary>
public int BinaryBufferSize { get; set; }
/// <summary>
/// Gets the encoding.
/// </summary>
public Encoding Encoding { get; private set; }
/// <summary>
/// Gets the mapping of parameters parsed files. The name of a given field
/// maps to the parsed file data.
/// </summary>
public List<FilePart> Files { get; private set; }
/// <summary>
/// Gets the mapping of the parameters. The name of a given field
/// maps to the parameter data.
/// </summary>
public Dictionary<string, ParameterPart> Parameters { get; private set; }
#endregion
#region Methods
/// <summary>
/// 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.
/// </summary>
/// <param name="reader">
/// The binary reader to parse
/// </param>
/// <returns>
/// The boundary string
/// </returns>
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.
var boundary = string.Concat(reader.ReadLine().Skip(2));
reader.Buffer("--" boundary "\n");
return boundary;
}
/// <summary>
/// Finds the next sequence of newlines in the input stream.
/// </summary>
/// <param name="data">The data to search</param>
/// <param name="offset">The offset to start searching at</param>
/// <param name="maxBytes">The maximum number of bytes (starting from offset) to search.</param>
/// <returns>The offset of the next newline</returns>
private int FindNextNewline(ref byte[] data, int offset, int maxBytes)
{
byte[][] newlinePatterns = { this.Encoding.GetBytes("\r\n"), this.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);
if (position != -1)
{
return position offset;
}
}
return -1;
}
/// <summary>
/// Calculates the length of the next found newline.
/// data[offset] is the start of the space to search.
/// </summary>
/// <param name="data">
/// The data containing the newline
/// </param>
/// <param name="offset">
/// The offset of the start of the newline
/// </param>
/// <returns>
/// The length in bytes of the newline sequence
/// </returns>
private int CalculateNewlineLength(ref byte[] data, int offset)
{
byte[][] newlinePatterns = { this.Encoding.GetBytes("\r\n"), this.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;
}
/// <summary>
/// Begins the parsing of the stream into objects.
/// </summary>
/// <param name="reader">
/// The multipart/form-data binary reader to parse from.
/// </param>
/// <exception cref="MultipartParseException">
/// thrown on finding unexpected data such as a boundary before we are ready for one.
/// </exception>
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 until we find a boundary
while (true)
{
string line = reader.ReadLine();
if (line == this.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 (!this.readEndBoundary)
{
// ParseSection will parse up to and including
// the next boundary.
this.ParseSection(reader);
}
}
/// <summary>
/// Parses a section of the stream that is known to be file data.
/// </summary>
/// <param name="parameters">
/// The header parameters of this file, expects "name" and "filename" to be valid keys
/// </param>
/// <param name="reader">
/// The StreamReader to read the data from
/// </param>
/// <returns>
/// The <see cref="FilePart"/> containing the parsed data (name, filename, stream containing file).
/// </returns>
private FilePart ParseFilePart(Dictionary<string, string> parameters, RebufferableBinaryReader reader)
{
// We want to create a stream and fill it with the data from the
// file.
var data = new MemoryStream();
var curBuffer = new byte[this.BinaryBufferSize];
var prevBuffer = new byte[this.BinaryBufferSize];
int curLength = 0;
int prevLength = 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
var fullBuffer = new byte[this.BinaryBufferSize * 2];
Buffer.BlockCopy(prevBuffer, 0, fullBuffer, 0, prevLength);
Buffer.BlockCopy(curBuffer, 0, fullBuffer, 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, this.endBoundaryBinary);
int endBoundaryLength = this.endBoundaryBinary.Length;
int boundaryPos = SubsequenceFinder.Search(fullBuffer, this.boundaryBinary);
int boundaryLength = this.boundaryBinary.Length;
// 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;
this.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;
this.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 = this.CalculateNewlineLength(ref fullBuffer, 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.
var maxNewlineBytes = Encoding.GetMaxByteCount(2);
int bufferNewlineOffset = this.FindNextNewline(
ref fullBuffer, Math.Max(0, endPos - maxNewlineBytes), maxNewlineBytes);
int bufferNewlineLength = this.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
data.Write(fullBuffer, 0, 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);
// stream.Write(fullBuffer, writeBackOffset, writeBackAmount);
// stream.Position = stream.Position - writeBackAmount;
// stream.Flush();
data.Position = 0;
data.Flush();
break;
}
// No end, consume the entire previous buffer
data.Write(prevBuffer, 0, prevLength);
data.Flush();
// 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);
var contentType = parameters.ContainsKey("content-type") ? parameters["content-type"] : "text/plain";
var contentDisposition = parameters.ContainsKey("content-disposition") ? parameters["content-disposition"] : "form-data";
var part = new FilePart(parameters["name"], parameters["filename"], data, contentType, contentDisposition);
return part;
}
/// <summary>
/// Parses a section of the stream that is known to be parameter data.
/// </summary>
/// <param name="parameters">
/// The header parameters of this section. "name" must be a valid key.
/// </param>
/// <param name="reader">
/// The StreamReader to read the data from
/// </param>
/// <returns>
/// The <see cref="ParameterPart"/> containing the parsed data (name, value).
/// </returns>
/// <exception cref="MultipartParseException">
/// thrown if unexpected data is found such as running out of stream before hitting the boundary.
/// </exception>
private ParameterPart ParseParameterPart(Dictionary<string, string> 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
// until we hit the boundary
var data = new StringBuilder();
//string line = reader.ReadLine();
string line = reader.ReadStringLine();
while (line != this.boundary && line != this.endBoundary)
{
if (line == null)
{
throw new MultipartParseException("Unexpected end of section");
}
data.Append(line);
line = reader.ReadLine();
//line = reader.ReadStringLine();
}
if (line == this.endBoundary)
{
this.readEndBoundary = true;
}
// If we're here we've hit the boundary and have the data!
var part = new ParameterPart(parameters["name"], data.ToString());
return part;
}
/// <summary>
/// Parses the header of the next section of the multipart stream and
/// determines if it contains file data or parameter data.
/// </summary>
/// <param name="reader">
/// The StreamReader to read data from.
/// </param>
/// <exception cref="MultipartParseException">
/// thrown if unexpected data is hit such as end of stream.
/// </exception>
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 practice it seems that multiple files will be represented by
// multiple Content-Disposition: form-data files.
var parameters = new Dictionary<string, string>();
string line = reader.ReadLine();
while (line != string.Empty)
{
if (line == null)
{
throw new MultipartParseException("Unexpected end of stream");
}
if (line == this.boundary || line == this.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<string, string> values = line.Split(';') // Split the line into n strings delimited by ;
.Select(x => x.Split(new[] { ':', '=' }))
.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 ex)
{
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.
FilePart part = this.ParseFilePart(parameters, reader);
this.Files.Add(part);
}
else
{
ParameterPart part = this.ParseParameterPart(parameters, reader);
this.Parameters.Add(part.Name, part);
}
}
#endregion
}
}
相关软件
小贴士
感谢您为本站写下的评论,您的评论对其它用户来说具有重要的参考价值,所以请认真填写。
- 类似“顶”、“沙发”之类没有营养的文字,对勤劳贡献的楼主来说是令人沮丧的反馈信息。
- 相信您也不想看到一排文字/表情墙,所以请不要反馈意义不大的重复字符,也请尽量不要纯表情的回复。
- 提问之前请再仔细看一遍楼主的说明,或许是您遗漏了。
- 请勿到处挖坑绊人、招贴广告。既占空间让人厌烦,又没人会搭理,于人于己都无利。
关于好例子网
本站旨在为广大IT学习爱好者提供一个非营利性互相学习交流分享平台。本站所有资源都可以被免费获取学习研究。本站资源来自网友分享,对搜索内容的合法性不具有预见性、识别性、控制性,仅供学习研究,请务必在下载后24小时内给予删除,不得用于其他任何用途,否则后果自负。基于互联网的特殊性,平台无法对用户传输的作品、信息、内容的权属或合法性、安全性、合规性、真实性、科学性、完整权、有效性等进行实质审查;无论平台是否已进行审查,用户均应自行承担因其传输的作品、信息、内容而可能或已经产生的侵权或权属纠纷等法律责任。本站所有资源不代表本站的观点或立场,基于网友分享,根据中国法律《信息网络传播权保护条例》第二十二与二十三条之规定,若资源存在侵权或相关问题请联系本站客服人员,点此联系我们。关于更多版权及免责申明参见 版权及免责申明


网友评论
我要评论