tesses.http/Tesses.Http/MultipartParser.cs

215 lines
7.2 KiB
C#

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Net.Mime;
namespace Tesses.Http
{
public sealed class MultipartParser
{
string boundary="";
HttpParser p;
public MultipartParser(HttpParser parser)
{
p=parser;
//boundary=----WebKitFormBoundaryyGTVag4OQVNjqdSl
string _bstr;
if(p.ReceivedHeaders.TryGetFirst("Content-Type",out _bstr))
{
//multipart/*; boundary=somevalue
int offsetOfBoundary = _bstr.IndexOf("boundary=");
if(!_bstr.StartsWith("multipart/")) throw new Exception("not a multipart request/response");
if(offsetOfBoundary < 0) throw new Exception("No \"boundary=\" in multipart header");
offsetOfBoundary+=9;
int offsetOfSemiAfterBoundary = _bstr.IndexOf(';',offsetOfBoundary);
if(offsetOfSemiAfterBoundary < 0)
{
boundary=_bstr.Substring(offsetOfBoundary);
}
else
{
boundary=_bstr.Substring(offsetOfBoundary,offsetOfSemiAfterBoundary - offsetOfBoundary);
}
//we got the boundary str
}else{
throw new Exception("No Content-Type found");
}
}
#region DAJURIC_SIMPLE_HTTP_CODE
public IEnumerable<MultipartEntry> Parse(OnFile onFile,bool disposeStream=true)
{
string bound = "--" + boundary;
//Console.WriteLine("BEFORE STREAM");
var inputStream = new BufferedStream(p.ReadBody());
//Print("Before ParseUntillBoundaryEnd");
//Console.WriteLine("Before ParseUntilBoundaryEnd");
parseUntillBoundaryEnd(inputStream, new MemoryStream(), bound);
//Console.WriteLine("After ParseUntilBoundaryEnd");
//Print("After ParseUntillBoundaryEnd");
while (true)
{
//Print("Before ParseSection");
var (n, v, fn, hdrs,ok) = parseSection(inputStream, "\r\n" + bound, onFile);
if(!ok) break;
//Print("After ParseSection");
v.Position = 0;
yield return new MultipartEntry(n,hdrs,fn,v);
if(disposeStream)
{
v.Dispose();
}
}
}
//this region of code is somewhat modified from https://github.com/dajuric/simple-http/
/// <summary>
/// Delegate executed when a file is about to be read from a body stream.
/// </summary>
/// <param name="fieldName">Field name.</param>
/// <param name="fileName">name of the file.</param>
/// <param name="contentType">Content type.</param>
/// <returns>Stream to be populated.</returns>
public delegate Stream OnFile(string fieldName, string fileName, string contentType);
private static bool parseUntillBoundaryEnd(Stream source, Stream destination, string boundary)
{
var checkBuffer = new byte[boundary.Length]; //for boundary checking
int b, i = 0;
while ((b = source.ReadByte()) != -1)
{
//Console.WriteLine(i);
if (i == boundary.Length) //boundary found -> go to the end of line
{
if (b == '\n') break;
if(b == '-') {
source.ReadByte();
return false;
}
continue;
}
if (b == boundary[i]) //start filling the check buffer
{
checkBuffer[i] = (byte)b;
i++;
}
else
{
var idx = 0;
while (idx < i) //write the buffer data to stream
{
destination.WriteByte(checkBuffer[idx]);
idx++;
}
i = 0;
destination.WriteByte((byte)b); //write the current byte
}
}
return true;
}
private static (string Name, Stream Value, string FileName, Dictionary<string,List<string>> headers,bool isOk) parseSection(Stream source, string boundary, OnFile onFile)
{
//we must read two bytes
HeaderCollection coll=new HeaderCollection(source);
ContentDisposition disposition=new ContentDisposition();
string ct="application/octet-stream";
string n="";
string fn="";
if(coll.ContainsKey("Content-Disposition"))
{
disposition=new ContentDisposition(coll["Content-Disposition"][0]);
n=disposition.Parameters["name"];
fn=disposition.FileName;
}
if(!coll.TryGetFirst("Content-Type",out ct))
{
ct="application/octet-stream";
}
//Print("Before ReadContentDisposition");
//var (n, fn, ct) = readContentDisposition(source);
//Print("After ReadContentDisposition");
//\r\n (empty row)
var dst = String.IsNullOrEmpty(fn) ? new MemoryStream() : onFile(n, fn, ct);
if (dst == null)
throw new ArgumentException(nameof(onFile), "The on-file callback must return a stream.");
//Print("Before ParseUntillBodyEnd");
bool isOk = parseUntillBoundaryEnd(source, dst, boundary);
//Print("Before ParseUntillBodyEnd");
return (n, dst, fn, coll,isOk);
}
#endregion
public IEnumerable<MultipartEntry> Parse(bool disposeStream=true)
{
return Parse((e,f,g)=>{return new MemoryStream();},disposeStream);
}
}
public class MultipartEntry : IDisposable
{
internal MultipartEntry(string name,Dictionary<string,List<string>> headers,string filename,Stream strm)
{
Name=name;
this.Stream=strm;
FileName = filename;
HasFileName=!string.IsNullOrWhiteSpace(filename);
Headers =headers;
string ct;
if(Headers.TryGetFirst("Content-Type",out ct))
{
this.MimeType=ct;
}
}
public Dictionary<string,List<string>> Headers {get;set;}
public bool HasFileName {get;private set;}
public string MimeType {get;private set;}
public string FileName {get;private set;}
public string Name {get;private set;}
public Stream Stream {get;private set;}
public void Dispose()
{
Stream.Dispose();
}
public byte[] GetData()
{
return Stream.AsArray();
}
public string GetStringData()
{
return GetData().AsString();
}
}
}