Fix path security bug and added features

This commit is contained in:
Mike Nolan 2024-03-18 07:31:51 -05:00
parent 5e557faaaf
commit ee57c7a3ca
10 changed files with 719 additions and 61 deletions

View File

@ -5,25 +5,48 @@
# License
Starting with 1.0.3.9 this library will use GPL-3.0
If you can not use GPL either use 1.0.3.8 or use another library
<br>
A TcpListener HTTP Server
WARNING: use at least version 1.0.4.2 because of security issue with paths
To make your life easier, install [Tesses.WebServer.EasyServer](https://www.nuget.org/packages/Tesses.WebServer.EasyServer) alongside [Tesses.WebServer](https://www.nuget.org/packages/Tesses.WebServer) and use this code:
```csharp
using Tesses.WebServer;
using Tesses.WebServer.EasyServer;
...
StaticServer server=new StaticServer(""); //or any server
...
server.StartServer(9500); //or any port number
//and it will print your ips to console with
//the message Almost ready to Listen
```
# Currently Supports
- GET+HEAD+POST Requests
- Seekable Video Files (Using Range)
- Can Send Json To Client with helper function (uses Newtonsoft.Json)
- Seekable Video Files (or any file) (Using Range)
- Can Send/Receive Json To/From Client with helper functions (uses Newtonsoft.Json)
- Cors Header
- Chunked encoding
- Tesses.IVirtualFileSystem support (Work in progress and buggy)
- wii-linux-ngx using this copy of [mono](https://tesses.net/apps/tytd/2022/wii.php)
# Classes To Make It Easier
- Static Website Class (Can pass in other class (instead of 404 when file doesnt exist) can choose other names other than index.html, index.htm, default.html, default.htm)
- 404 Not Found Class
- Mount class (So you could use Multiple Apis, And Static Sites If you want)
- Basic Auth Class
- Route Class (Just like dajuric/simple-http)
- Host Name Class (like Mount Class but is used for hostnames/ip addresses like tesses.cf, 192.168.0.142, demilovato.com, ebay.com)
- Route Class (Just like dajuric/simple-http, except it uses query parameters)
- Host Name Class (like Mount Class but is used for hostnames/ip addresses like tesses.net, 192.168.0.142, demilovato.com, ebay.com, tessesstudios.com, godworldwide.org)
- Path Value Class (can handle paths like this /user/Jehovah/files where Jehovah is the path element)
# Might Happen But not sure
- WebDav Class
- WebDav Class (and may be used in a seperate library)
- Reverse Proxy (in a seperate library)
> Note: Range code, POST code and Route Class is not mine its a modified version of the code from ( [dajuric/simple-http](https://github.com/dajuric/simple-http/blob/master/Source/SimpleHTTP/Extensions/Response/ResponseExtensions.PartialStream.cs "dajuric/simple-http"))

View File

@ -0,0 +1,19 @@

using System.Net.Http.Json;
using System.Text;
HttpClient client = new HttpClient();
HttpRequestMessage requestMessage=new HttpRequestMessage(HttpMethod.Get,"http://localhost:24240/api/route/jsonEndpoint");
requestMessage.Headers.Add("Authorization",$"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes("demi:password123"))}");
requestMessage.Content = JsonContent.Create(new Obj());
var resp=await client.SendAsync(requestMessage);
Console.WriteLine(await resp.Content.ReadAsStringAsync());
public class Obj
{
public string Name {get;set;}="Demetria Devonne Lovato";
public DateTime Birthday {get;set;}=new DateTime(1992,8,20);
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -2,7 +2,12 @@
using Tesses.WebServer;
namespace Tesses.WebServer.ConsoleApp
{
class JsonObj
{
public string Name {get;set;}="";
public DateTime Birthday {get;set;}=DateTime.Now;
}
class MainClass
{
public static void Main(string[] args)
@ -16,6 +21,27 @@ namespace Tesses.WebServer.ConsoleApp
await ctx.SendTextAsync("Demetria Devonne Lovato 8/20/1992");
});
rserver.Add("/jsonEndpoint",async(ctx)=>{
var res=await ctx.ReadJsonAsync<JsonObj>();
if(res !=null)
{
Console.WriteLine($"Name: {res.Name}");
Console.WriteLine($"Birthday: {res.Birthday.ToShortDateString()}");
await ctx.SendTextAsync("The meaning of life is 42","text/plain");
}
});
rserver.Add("/typewriter",(ctx)=>{
using(var sw=ctx.GetResponseStreamWriter("text/plain"))
{
foreach(var c in "This is a typewriter\nwell this is cool\nThis is thanks to the chunked stream.")
{
sw.Write(c);
sw.Flush();
System.Threading.Thread.Sleep(50);
}
}
});
var ip=System.Net.IPAddress.Any;
StaticServer static_server = new StaticServer(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyVideos));
MountableServer mountable = new MountableServer(static_server);

View File

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\Tesses.WebServer.NetStandard\Tesses.WebServer.NetStandard.csproj" />
<ItemGroup>
<ProjectReference Include="..\Tesses.WebServer.NetStandard\Tesses.WebServer.NetStandard.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@ -19,7 +19,7 @@ namespace Tesses.WebServer.ConsoleApp
HttpServerListener s = new HttpServerListener(new System.Net.IPEndPoint(ip, 24240),static_server);
s.ListenAsync(System.Threading.CancellationToken.None).Wait();
}

View File

@ -2,6 +2,7 @@
using System.IO;
using System;
using System.Net;
using System.Text;
namespace Tesses.WebServer
{
@ -70,6 +71,20 @@ internal class SizedStream : Stream
}
public class ServerContext
{
const string bad_chars = "<>?/\\\"*|:";
public static string FixFileName(string filename,bool requireAscii=false)
{
StringBuilder builder=new StringBuilder();
foreach(var c in filename)
{
if(char.IsControl(c)) continue;
if(requireAscii && c > 127) continue;
if(bad_chars.Contains(c.ToString())) continue;
builder.Append(c);
}
return builder.ToString();
}
/// <summary>
/// Some user data
/// </summary>
@ -277,6 +292,10 @@ internal class SizedStream : Stream
return new SizedStream(NetworkStream,len);
}
}
else if(RequestHeaders.TryGetFirst("Transfer-Encoding",out var res) && res == "chunked")
{
return new ChunkedStream(NetworkStream,true);
}
//DajuricSimpleHttpExtensions.Print("Returns NetworkStream");
return NetworkStream;
}

View File

@ -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>

View File

@ -5,14 +5,14 @@
<PackageId>Tesses.WebServer</PackageId>
<Author>Mike Nolan</Author>
<Company>Tesses</Company>
<Version>1.0.4.1</Version>
<AssemblyVersion>1.0.4.1</AssemblyVersion>
<FileVersion>1.0.4.1</FileVersion>
<Version>1.0.4.2</Version>
<AssemblyVersion>1.0.4.2</AssemblyVersion>
<FileVersion>1.0.4.2</FileVersion>
<Description>A TCP Listener HTTP(s) Server</Description>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageTags>HTTP, WebServer, Website</PackageTags>
<RepositoryUrl>https://gitlab.tesses.cf/tesses50/tesses.webserver</RepositoryUrl>
<RepositoryUrl>https://gitlab.tesses.net/tesses50/tesses.webserver</RepositoryUrl>
</PropertyGroup>
<ItemGroup>

View File

@ -14,6 +14,7 @@ using System.Net.Security;
using System.Security.Authentication;
using System.Web;
using Tesses.VirtualFilesystem;
using System.Net.Mime;
namespace Tesses.WebServer
{
@ -42,6 +43,58 @@ namespace Tesses.WebServer
public static class Extensions
{
public static ServerContext WithStatusCode(this ServerContext ctx, HttpStatusCode statusCode)
{
ctx.StatusCode = (int)statusCode;
return ctx;
}
public static void SendNotFound(this ServerContext ctx)
{
ctx.StatusCode=404;
string url=WebUtility.HtmlEncode(ctx.OriginalUrlPath);
ctx.SendText($"<html><head><title>File {url} not found</title></head><body><h1>404 Not Found</h1><h4>{url}</h4></body></html>");
}
public static async Task SendNotFoundAsync(this ServerContext ctx)
{
ctx.StatusCode=404;
string url=WebUtility.HtmlEncode(ctx.OriginalUrlPath);
await ctx.SendTextAsync($"<html><head><title>File {url} not found</title></head><body><h1>404 Not Found</h1><h4>{url}</h4></body></html>");
}
public static ServerContext WithDate(this ServerContext ctx, DateTime dateTime)
{
ctx.ResponseHeaders.Add("Date",dateTime.ToString("R"));
return ctx;
}
public static ServerContext WithLastModified(this ServerContext ctx, DateTime dateTime)
{
ctx.ResponseHeaders.Add("Last-Modified",dateTime.ToString("R"));
return ctx;
}
public static ServerContext WithMimeType(this ServerContext ctx, string mimeType)
{
if(ctx.ResponseHeaders.ContainsKey("Content-Type"))
{
ctx.ResponseHeaders["Content-Type"].Clear();
ctx.ResponseHeaders["Content-Type"].Add(mimeType);
}
else
{
ctx.ResponseHeaders.Add("Content-Type",mimeType);
}
return ctx;
}
public static ServerContext WithMimeTypeFromFileName(this ServerContext ctx, string filename)
{
return ctx.WithMimeType(HeyRed.Mime.MimeTypesMap.GetMimeType(filename));
}
public static ServerContext WithFileName(this ServerContext ctx, string filename, bool inline)
{
ContentDisposition disposition=new ContentDisposition();
disposition.FileName = filename;
disposition.Inline = inline;
ctx.ResponseHeaders.Add("Content-Disposition",disposition.ToString());
return ctx;
}
public static async Task WriteAsync(this Stream strm,string text)
{
var data=Encoding.UTF8.GetBytes(text);
@ -82,13 +135,36 @@ namespace Tesses.WebServer
public static async Task<string> ReadStringAsync(this ServerContext ctx)
{
string str = null;
using (var reader = new StreamReader(ctx.GetRequestStream()))
using (var reader = ctx.GetRequestStreamReader())
{
str = await reader.ReadToEndAsync();
}
return str;
}
public static StreamReader GetRequestStreamReader(this ServerContext ctx)
{
return new StreamReader(ctx.GetRequestStream(),Encoding.UTF8,false,4096,false);
}
public static StreamWriter GetResponseStreamWriter(this ServerContext ctx,string mimetype)
{
return ctx.WithMimeType(mimetype).GetResponseStreamWriter();
}
public static StreamWriter GetResponseStreamWriter(this ServerContext ctx)
{
return new StreamWriter(ctx.GetResponseStream(),Encoding.UTF8,4096,false);
}
public static Stream GetResponseStream(this ServerContext ctx)
{
ctx.ResponseHeaders.Add("Transfer-Encoding","chunked");
ctx.WriteHeaders();
return new ChunkedStream(ctx.NetworkStream,false);
}
public static Stream GetResponseStream(this ServerContext ctx, string mimetype)
{
return ctx.WithMimeType(mimetype).GetResponseStream();
}
/// <summary>
/// Read string from request body
/// </summary>
@ -97,7 +173,7 @@ namespace Tesses.WebServer
public static string ReadString(this ServerContext ctx)
{
string str = null;
using (var reader = new StreamReader(ctx.GetRequestStream()))
using (var reader = ctx.GetRequestStreamReader())
{
str = reader.ReadToEnd();
}
@ -214,7 +290,7 @@ namespace Tesses.WebServer
}
return filename;
}
/// <summary>
/// Write headers to stream
/// </summary>
@ -255,32 +331,7 @@ namespace Tesses.WebServer
var data = Encoding.UTF8.GetBytes(b.ToString());
ctx.NetworkStream.Write(data, 0, data.Length);
}
/// <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)
{
using (var strm = File.OpenRead(file))
{
await ctx.SendStreamAsync( strm, MimeTypesMap.GetMimeType(file));
}
}
/// <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)
{
using (var strm = File.OpenRead(file))
{
ctx.SendStream( strm, MimeTypesMap.GetMimeType(file));
}
}
/// <summary>
/// Send exception to client
/// </summary>
@ -290,7 +341,7 @@ namespace Tesses.WebServer
public static async Task SendExceptionAsync(this ServerContext ctx, Exception ex)
{
string name = ex.GetType().FullName;
string j = $"<html><head><title>{WebUtility.HtmlEncode(name)} thrown</title></head><body><h1>{WebUtility.HtmlEncode(name)} thrown</h1><h3>Description: {WebUtility.HtmlEncode(ex.Message)}</h3></body></html>";
string j = $"<html><head><title>{WebUtility.HtmlEncode(name)} thrown</title></head><body><h1>{WebUtility.HtmlEncode(name)} thrown</h1><h3>Description: {WebUtility.HtmlEncode(ex.ToString())}</h3></body></html>";
ctx.StatusCode = 500;
await ctx.SendTextAsync(j);
}
@ -303,7 +354,7 @@ namespace Tesses.WebServer
public static void SendException(this ServerContext ctx, Exception ex)
{
string name = ex.GetType().FullName;
string j = $"<html><head><title>{WebUtility.HtmlEncode(name)} thrown</title></head><body><h1>{WebUtility.HtmlEncode(name)} thrown</h1><h3>Description: {WebUtility.HtmlEncode(ex.Message)}</h3></body></html>";
string j = $"<html><head><title>{WebUtility.HtmlEncode(name)} thrown</title></head><body><h1>{WebUtility.HtmlEncode(name)} thrown</h1><h3>Description: {WebUtility.HtmlEncode(ex.ToString())}</h3></body></html>";
ctx.StatusCode = 500;
ctx.SendText(j);
}
@ -512,6 +563,8 @@ namespace Tesses.WebServer
{
}
}
public class SameServer : Server
@ -534,6 +587,7 @@ namespace Tesses.WebServer
ctx.StatusCode = _statusCode;
await ctx.SendTextAsync( _html.Replace("{url}", WebUtility.HtmlEncode(ctx.OriginalUrlPath)));
}
}
public enum WebServerPathType
@ -693,9 +747,23 @@ namespace Tesses.WebServer
name = "";
return false;
}
//fix a nasty security risk, that allows people to access parent files in filesystem
private string FixDotPath(string url)
{
List<string> strs=new List<string>();
foreach(var item in url.Split(new char[]{'/'}, StringSplitOptions.RemoveEmptyEntries))
{
if(item != ".." && item != ".")
{
strs.Add(item);
}
}
return string.Join("/",strs);
}
public WebServerPathEntry GetPath(string url)
{
string someUrl = Path.Combine(_path,WebUtility.UrlDecode(url.Substring(1)).Replace('/', Path.DirectorySeparatorChar));
string someUrl = Path.Combine(_path,WebUtility.UrlDecode(FixDotPath(url)).Replace('/', Path.DirectorySeparatorChar));
//Console.WriteLine(someUrl);
if (Directory.Exists(someUrl))
{
@ -967,12 +1035,13 @@ namespace Tesses.WebServer
_forbidden = new SameServer("<html><head><title>File {url} not accessable</title></head><body><h1>403 Forbidden</h1><h4>{url}</h4></body></html>",403);
}
public bool RedirectToRootInsteadOfNotFound {get;set;}=false;
/// <summary>
/// construct with path and with option to allow/deny listing directories
/// </summary>
/// <param name="path">directory for server</param>
/// <param name="allowListing">whether to allow listing directory or not (overridable by environment variable with TESSES_WEBSERVER_ALLOW_LISTING=true|false)
public bool RedirectToRootInsteadOfNotFound {get;set;}=false;
public StaticServer(string path,bool allowListing)
{
_server = new NotFoundServer();
@ -1240,6 +1309,124 @@ namespace Tesses.WebServer
}
}
/// <summary>
/// use values inside path like this /hello/YOUR_VALUE/somepage
/// </summary>
public class PathValueServer : Server
{
public IServer Server {get;set;}
int paths;
/// <summary>
/// Construct a PathValueServer with NotFoundServer
/// </summary>
/// <param name="paths">How many path parts do you want, eg if your path is /jim/john/joel, 1: jim 2: jim/john 3: jim/john/joel</param>
public PathValueServer(int paths=1)
{
Server = new NotFoundServer();
this.paths=paths;
}
/// <summary>
/// Construct a PathValueServer with your server
/// </summary>
/// <param name="inner">Your server</param>
/// <param name="paths">How many path parts do you want, eg if your path is /jim/john/joel, 1: jim 2: jim/john 3: jim/john/joel</param>
public PathValueServer(IServer inner,int paths=1)
{
Server = inner;
this.paths = paths;
}
/// <summary>
/// Use this inside your inner server to get string value
/// </summary>
/// <param name="ctx">the ServerContext</param>
/// <returns>the string value from the url part</returns>
public string GetValue(ServerContext ctx)
{
lock(kvps)
{
if(kvps.ContainsKey(ctx))
{
return kvps[ctx];
}
}
return "";
}
private void SetValue(ServerContext ctx,string val)
{
lock(kvps)
{
if(kvps.ContainsKey(ctx))
{
kvps[ctx] = val;
}
else
{
kvps.Add(ctx,val);
}
}
}
private void RemoveValue(ServerContext ctx)
{
lock(kvps)
{
kvps.Remove(ctx);
}
}
Dictionary<ServerContext,string> kvps=new Dictionary<ServerContext, string>();
public override async Task GetAsync(ServerContext ctx)
{
string[] path=ctx.UrlPath.Split(new char[]{'/'},System.StringSplitOptions.RemoveEmptyEntries);
SetValue(ctx,string.Join("/",path.Take(paths)));
ctx.UrlPath = $"/{string.Join("/",path.Skip(paths))}";
await Server.GetAsync(ctx);
RemoveValue(ctx);
}
public override async Task OptionsAsync(ServerContext ctx)
{
string[] path=ctx.UrlPath.Split(new char[]{'/'},System.StringSplitOptions.RemoveEmptyEntries);
SetValue(ctx,string.Join("/",path.Take(paths)));
ctx.UrlPath = $"/{string.Join("/",path.Skip(paths))}";
await Server.OptionsAsync(ctx);
RemoveValue(ctx);
}
public override async Task OtherAsync(ServerContext ctx)
{
string[] path=ctx.UrlPath.Split(new char[]{'/'},System.StringSplitOptions.RemoveEmptyEntries);
SetValue(ctx,string.Join("/",path.Take(paths)));
ctx.UrlPath = $"/{string.Join("/",path.Skip(paths))}";
await Server.OtherAsync(ctx);
RemoveValue(ctx);
}
public override async Task PostAsync(ServerContext ctx)
{
string[] path=ctx.UrlPath.Split(new char[]{'/'},System.StringSplitOptions.RemoveEmptyEntries);
SetValue(ctx,string.Join("/",path.Take(paths)));
ctx.UrlPath = $"/{string.Join("/",path.Skip(paths))}";
await Server.PostAsync(ctx);
RemoveValue(ctx);
}
public override async Task<bool> BeforeAsync(ServerContext ctx)
{
var urlPath = ctx.UrlPath;
string[] path=ctx.UrlPath.Split(new char[]{'/'},System.StringSplitOptions.RemoveEmptyEntries);
SetValue(ctx,string.Join("/",path.Take(paths)));
ctx.UrlPath = $"/{string.Join("/",path.Skip(paths))}";
var r= await Server.BeforeAsync(ctx);
ctx.UrlPath = urlPath;
return r;
}
}
/// <summary>
/// Abstract class for server
/// </summary>
public abstract class Server : IServer
@ -1327,6 +1514,36 @@ namespace Tesses.WebServer
}
}
public static Server FromCallback(HttpActionAsync cb)
{
return new CallbackServer(cb);
}
private class CallbackServer : Server
{
HttpActionAsync cb;
public CallbackServer(HttpActionAsync cb)
{
this.cb = cb;
}
public override async Task GetAsync(ServerContext ctx)
{
await cb(ctx);
}
public override async Task OptionsAsync(ServerContext ctx)
{
await cb(ctx);
}
public override async Task OtherAsync(ServerContext ctx)
{
await cb(ctx);
}
public override async Task PostAsync(ServerContext ctx)
{
await cb(ctx);
}
}
}
/// <summary>
/// mount multiple servers at different url paths
@ -1652,6 +1869,140 @@ namespace Tesses.WebServer
Task OtherAsync(ServerContext ctx);
}
public sealed class ChunkedStream : Stream
{
int offset=0;
byte[] buffer=new byte[0];
Stream strm;
bool receive;
public ChunkedStream(Stream strm, bool receive)
{
this.receive = receive;
this.strm=strm;
}
public override bool CanRead => receive;
public override bool CanSeek => false;
public override bool CanWrite => !receive;
public override long Length => throw new NotImplementedException();
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public override void Flush()
{
strm.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
if(!receive) throw new IOException("Cannot read from a writeonly stream");
if(EOF) throw new EndOfStreamException();
if(this.offset>=this.buffer.Length)
{
//we need to read a new one
StringBuilder b=new StringBuilder();
int read = 0;
while((read=strm.ReadByte()) != '\r')
{
b.Append((char)read);
}
if(read == '\r')
{
read=strm.ReadByte();
if(read != '\n')
{
throw new IOException("Must end with \r\n");
}
}
else
{
throw new IOException("Must end with \r\n");
}
if(int.TryParse(b.ToString(),System.Globalization.NumberStyles.HexNumber,null,out int val))
{
if(val == 0)
{
EOF=true;
if(strm.ReadByte()!='\r' || strm.ReadByte() != '\n')
{
throw new IOException("Must end with \r\n");
}
return 0;
}
this.offset = 0;
this.buffer=new byte[val];
strm.Read(this.buffer,0,this.buffer.Length);
if(strm.ReadByte()!='\r' || strm.ReadByte() != '\n')
{
throw new IOException("Must end with \r\n");
}
}
}
count = Math.Min(count,this.buffer.Length-this.offset);
Array.Copy(this.buffer,this.offset,buffer,offset,count);
this.offset+=count;
return count;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotImplementedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
if(receive) throw new IOException("Cannot write to readonly stream");
if(count == 0 && EOF) return;
var len = System.Text.Encoding.ASCII.GetBytes($"{count.ToString("X")}\r\n");
strm.Write(len,0,len.Length);
strm.Write(buffer,offset,count);
strm.Write(crlf,0,crlf.Length);
if(count==0) EOF=true;
}
private static readonly byte[] crlf = new byte[]{(byte)'\r',(byte)'\n'};
public bool EOF {get;private set;}=false;
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
if(receive) throw new IOException("Cannot write to readonly stream");
if(count == 0 && EOF) return;
var len = System.Text.Encoding.ASCII.GetBytes($"{count.ToString("X")}\r\n");
await strm.WriteAsync(len,0,len.Length,cancellationToken);
await strm.WriteAsync(buffer,offset,count,cancellationToken);
await strm.WriteAsync(crlf,0,crlf.Length,cancellationToken);
if(count==0) EOF=true;
}
protected override void Dispose(bool disposing)
{
if(disposing)
{
if(!EOF)
{
if(!receive)
Write(crlf,0,0);
}
}
}
}
public sealed class HttpServerListener
{
/// <summary>
@ -1861,8 +2212,7 @@ namespace Tesses.WebServer
{
StringBuilder s = new StringBuilder();
var decoder = Encoding.UTF8.GetDecoder();
var nextChar = new char[1];
while (!s.EndsWith("\r\n\r\n",StringComparison.Ordinal))
{
int data = strm.ReadByte();
@ -1870,9 +2220,8 @@ namespace Tesses.WebServer
{
break;
}
int charCount=decoder.GetChars(new byte[] { (byte)data }, 0, 1, nextChar, 0);
if (charCount == 0) continue;
s.Append(nextChar);
if(data > 127) continue;
s.Append((char)data); //its ascii now
}
return s.ToString();