Fix path security bug and added features
This commit is contained in:
@@ -7,6 +7,7 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using HeyRed.Mime;
|
||||
|
||||
namespace Tesses.WebServer
|
||||
{
|
||||
@@ -82,6 +83,7 @@ namespace Tesses.WebServer
|
||||
{
|
||||
try
|
||||
{
|
||||
var strm2=new ChunkedStream(ctx.NetworkStream,false);
|
||||
long tread=0;
|
||||
byte[] buffer=new byte[8*1024*1024];
|
||||
int read=0;
|
||||
@@ -94,7 +96,7 @@ namespace Tesses.WebServer
|
||||
}
|
||||
if(read == 0) break;
|
||||
read = strm.Read(buffer,0,read);
|
||||
strm.Write(buffer,0,read);
|
||||
strm2.Write(buffer,0,read);
|
||||
}while(read > 0);
|
||||
} finally {
|
||||
strm.Close();
|
||||
@@ -105,6 +107,7 @@ namespace Tesses.WebServer
|
||||
{
|
||||
try
|
||||
{
|
||||
var strm2=new ChunkedStream(ctx.NetworkStream,false);
|
||||
long tread=0;
|
||||
byte[] buffer=new byte[8*1024*1024];
|
||||
int read=0;
|
||||
@@ -117,7 +120,7 @@ namespace Tesses.WebServer
|
||||
}
|
||||
if(read == 0) break;
|
||||
read = await strm.ReadAsync(buffer,0,read);
|
||||
await strm.WriteAsync(buffer,0,read);
|
||||
await strm2.WriteAsync(buffer,0,read);
|
||||
}while(read > 0);
|
||||
} finally {
|
||||
strm.Close();
|
||||
@@ -151,7 +154,8 @@ namespace Tesses.WebServer
|
||||
|
||||
}
|
||||
ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString());
|
||||
ctx.ResponseHeaders.Add("Content-Type", contentType);
|
||||
|
||||
ctx.WithMimeType(contentType);
|
||||
|
||||
ctx.WriteHeaders();
|
||||
if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal))
|
||||
@@ -214,8 +218,118 @@ namespace Tesses.WebServer
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Send file to client (supports range partial content)
|
||||
/// </summary>
|
||||
/// <param name="ctx">ServerContext</param>
|
||||
/// <param name="file">the file to serve</param>
|
||||
|
||||
public static async Task SendFileAsync(this ServerContext ctx, string file)
|
||||
{
|
||||
if(!File.Exists(file))
|
||||
{
|
||||
await ctx.SendNotFoundAsync();
|
||||
return;
|
||||
}
|
||||
if(!ctx.RequestHeaders.ContainsKey(BYTES_RANGE_HEADER) && handleIfCached())
|
||||
return;
|
||||
|
||||
using(var f = File.OpenRead(file))
|
||||
{
|
||||
await ctx.SendStreamAsync(f,MimeTypesMap.GetMimeType(file));
|
||||
}
|
||||
|
||||
static Dictionary<string, HttpFile> ParseMultipartForm(ServerContext serverCtx, OnFile onFile)
|
||||
bool handleIfCached()
|
||||
{
|
||||
var lastModified = File.GetLastWriteTimeUtc(file);
|
||||
string etag=lastModified.Ticks.ToString("x");
|
||||
ctx.ResponseHeaders.Add("ETag",etag);
|
||||
ctx.ResponseHeaders.Add("Last-Modified",lastModified.ToString("R"));
|
||||
|
||||
if(ctx.RequestHeaders.TryGetFirst("If-None-Match",out var ifNoneMatch))
|
||||
{
|
||||
var eTags = ifNoneMatch.Split(',').Select(x => x.Trim()).ToArray();
|
||||
if (eTags.Contains(etag))
|
||||
{
|
||||
ctx.StatusCode = 304;
|
||||
ctx.WriteHeaders();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(ctx.RequestHeaders.TryGetFirst("If-Modified-Since",out var iMs) && DateTime.TryParse(iMs,out DateTime ifModifiedSince))
|
||||
{
|
||||
if (lastModified <= ifModifiedSince)
|
||||
{
|
||||
ctx.StatusCode = 304;
|
||||
ctx.WriteHeaders();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Send file to client (supports range partial content)
|
||||
/// </summary>
|
||||
/// <param name="ctx">ServerContext</param>
|
||||
/// <param name="file">the file to serve</param>
|
||||
|
||||
public static void SendFile(this ServerContext ctx, string file)
|
||||
{
|
||||
if(!File.Exists(file))
|
||||
{
|
||||
ctx.SendNotFound();
|
||||
return;
|
||||
}
|
||||
if(!ctx.RequestHeaders.ContainsKey(BYTES_RANGE_HEADER) && handleIfCached())
|
||||
return;
|
||||
|
||||
using(var f = File.OpenRead(file))
|
||||
{
|
||||
ctx.SendStream(f,MimeTypesMap.GetMimeType(file));
|
||||
}
|
||||
|
||||
bool handleIfCached()
|
||||
{
|
||||
var lastModified = File.GetLastWriteTimeUtc(file);
|
||||
string etag=lastModified.Ticks.ToString("x");
|
||||
ctx.ResponseHeaders.Add("ETag",etag);
|
||||
ctx.ResponseHeaders.Add("Last-Modified",lastModified.ToString("R"));
|
||||
|
||||
if(ctx.RequestHeaders.TryGetFirst("If-None-Match",out var ifNoneMatch))
|
||||
{
|
||||
var eTags = ifNoneMatch.Split(',').Select(x => x.Trim()).ToArray();
|
||||
if (eTags.Contains(etag))
|
||||
{
|
||||
ctx.StatusCode = 304;
|
||||
ctx.WriteHeaders();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if(ctx.RequestHeaders.TryGetFirst("If-Modified-Since",out var iMs) && DateTime.TryParse(iMs,out DateTime ifModifiedSince))
|
||||
{
|
||||
if (lastModified <= ifModifiedSince)
|
||||
{
|
||||
ctx.StatusCode = 304;
|
||||
ctx.WriteHeaders();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, List<HttpFile>> ParseMultipartForm(ServerContext serverCtx, OnFile onFile)
|
||||
{
|
||||
var args = serverCtx.QueryParams;
|
||||
string content_type=serverCtx.RequestHeaders.GetFirst("Content-Type");
|
||||
@@ -226,7 +340,7 @@ namespace Tesses.WebServer
|
||||
boundary = "--" + boundary;
|
||||
|
||||
|
||||
var files = new Dictionary<string, HttpFile>();
|
||||
var files = new Dictionary<string, List<HttpFile>>();
|
||||
var inputStream = new BufferedStream(serverCtx.GetRequestStream());
|
||||
//Print("Before ParseUntillBoundaryEnd");
|
||||
|
||||
@@ -362,7 +476,7 @@ namespace Tesses.WebServer
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses body of the request including form and multi-part form data.
|
||||
/// Parses body of the request including form and multi-part form data, allowing multiple file with same key.
|
||||
/// </summary>
|
||||
/// <param name="request">HTTP request.</param>
|
||||
/// <param name="args">Key-value pairs populated by the form data by this function.</param>
|
||||
@@ -371,7 +485,7 @@ namespace Tesses.WebServer
|
||||
/// <para>By default, <see cref="MemoryStream"/> is used, but for large files, it is recommended to open <see cref="FileStream"/> directly.</para>
|
||||
/// </param>
|
||||
/// <returns>Name-file pair collection.</returns>
|
||||
public static Dictionary<string, HttpFile> ParseBody(this ServerContext request, OnFile onFile)
|
||||
public static Dictionary<string, List<HttpFile>> ParseBodyMultiple(this ServerContext request, OnFile onFile)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
@@ -383,21 +497,119 @@ namespace Tesses.WebServer
|
||||
throw new ArgumentNullException(nameof(onFile));
|
||||
|
||||
|
||||
var files = new Dictionary<string, HttpFile>();
|
||||
|
||||
string content_type = request.RequestHeaders.GetFirst("Content-Type");
|
||||
if (content_type.StartsWith("application/x-www-form-urlencoded",StringComparison.Ordinal))
|
||||
{
|
||||
ParseForm(request);
|
||||
var files = new Dictionary<string, List<HttpFile>>();
|
||||
return files;
|
||||
}
|
||||
else if (content_type.StartsWith("multipart/form-data",StringComparison.Ordinal))
|
||||
{
|
||||
files = ParseMultipartForm(request, onFile);
|
||||
return ParseMultipartForm(request, onFile);
|
||||
}
|
||||
else
|
||||
throw new NotSupportedException("The body content-type is not supported.");
|
||||
|
||||
|
||||
}
|
||||
/// <summary>
|
||||
/// Parses body of the request including form and multi-part form data.
|
||||
/// </summary>
|
||||
/// <param name="request">HTTP request.</param>
|
||||
/// <param name="args">Key-value pairs populated by the form data by this function.</param>
|
||||
/// <param name="onFile">
|
||||
/// Function called if a file is about to be parsed. The stream is attached to a corresponding <see cref="HttpFile"/>.
|
||||
/// <para>By default, <see cref="MemoryStream"/> is used, but for large files, it is recommended to open <see cref="FileStream"/> directly.</para>
|
||||
/// </param>
|
||||
/// <returns>Name-file pair collection.</returns>
|
||||
public static Dictionary<string, HttpFile> ParseBody(this ServerContext request, OnFile onFile)
|
||||
{
|
||||
var res=ParseBodyMultiple(request,onFile);
|
||||
Dictionary<string,HttpFile> files = new Dictionary<string, HttpFile>();
|
||||
foreach(var item in res)
|
||||
{
|
||||
if(item.Value.Count > 0)
|
||||
{
|
||||
files.Add(item.Key,item.Value[0]);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
/// <summary>
|
||||
/// Parses body of the request including form and multi-part form data, allowing multiple file with same key and storing the files in a temp directory specified by the user.
|
||||
/// </summary>
|
||||
/// <param name="request">HTTP request.</param>
|
||||
/// <param name="tempDir">The root directory to store all the uploads in</param>
|
||||
/// <returns>A HttpFileResponse Containing Paths to files, Dispose only deletes the files, so if you want to keep the files don't dispose it.</returns>
|
||||
public static HttpFileResponse ParseBodyWithTempDirectory(this ServerContext request, string tempDir)
|
||||
{
|
||||
Directory.CreateDirectory(tempDir);
|
||||
|
||||
|
||||
Stream Open(string field, string filename, string contentType)
|
||||
{
|
||||
string dir=Path.Combine(tempDir,ServerContext.FixFileName(field));
|
||||
Directory.CreateDirectory(dir);
|
||||
string filename2 = Path.Combine(dir,ServerContext.FixFileName(filename));
|
||||
return File.Create(filename2);
|
||||
}
|
||||
List<HttpFileResponseEntry> responseEntries=new List<HttpFileResponseEntry>();
|
||||
foreach(var item in ParseBodyMultiple(request,Open))
|
||||
{
|
||||
foreach(var i2 in item.Value)
|
||||
{
|
||||
responseEntries.Add(new HttpFileResponseEntry(Path.Combine(tempDir,ServerContext.FixFileName(item.Key),ServerContext.FixFileName(i2.FileName)),i2.FileName,item.Key,i2.ContentType));
|
||||
i2.Dispose();
|
||||
}
|
||||
}
|
||||
return new HttpFileResponse(tempDir,responseEntries);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class HttpFileResponseEntry
|
||||
{
|
||||
public HttpFileResponseEntry(string path, string filename, string fieldname, string contype)
|
||||
{
|
||||
Path = path;
|
||||
FileName = filename;
|
||||
ContentType = contype;
|
||||
FieldName = fieldname;
|
||||
}
|
||||
public string FileName {get;}
|
||||
|
||||
public string ContentType {get;}
|
||||
|
||||
public string Path {get;}
|
||||
|
||||
public string FieldName {get;}
|
||||
|
||||
public FileInfo FileInfo => new FileInfo(Path);
|
||||
|
||||
public Stream OpenRead()
|
||||
{
|
||||
return File.OpenRead(Path);
|
||||
}
|
||||
public void MoveTo(string dest)
|
||||
{
|
||||
File.Move(Path,dest);
|
||||
}
|
||||
}
|
||||
public sealed class HttpFileResponse : IDisposable
|
||||
{
|
||||
public HttpFileResponse(string dir, IReadOnlyList<HttpFileResponseEntry> entries)
|
||||
{
|
||||
Directory = dir;
|
||||
Files = entries;
|
||||
}
|
||||
public IReadOnlyList<HttpFileResponseEntry> Files {get;}
|
||||
public string Directory {get;}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
System.IO.Directory.Delete(Directory,true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user