diff --git a/README.md b/README.md
index d9ee975..dd5761f 100644
--- a/README.md
+++ b/README.md
@@ -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
-
+
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"))
diff --git a/Tesses.WebServer.ClientTest/Program.cs b/Tesses.WebServer.ClientTest/Program.cs
new file mode 100644
index 0000000..624995b
--- /dev/null
+++ b/Tesses.WebServer.ClientTest/Program.cs
@@ -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);
+}
\ No newline at end of file
diff --git a/Tesses.WebServer.ClientTest/Tesses.WebServer.ClientTest.csproj b/Tesses.WebServer.ClientTest/Tesses.WebServer.ClientTest.csproj
new file mode 100644
index 0000000..206b89a
--- /dev/null
+++ b/Tesses.WebServer.ClientTest/Tesses.WebServer.ClientTest.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/Tesses.WebServer.Console/Program.cs b/Tesses.WebServer.Console/Program.cs
index 84f10c5..9419fb4 100644
--- a/Tesses.WebServer.Console/Program.cs
+++ b/Tesses.WebServer.Console/Program.cs
@@ -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();
+ 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);
diff --git a/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj b/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj
index aee68de..263c165 100644
--- a/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj
+++ b/Tesses.WebServer.Console/Tesses.WebServer.Console.csproj
@@ -1,12 +1,12 @@
-
-
+
+
Exe
- net6.0
+ net8.0
enable
enable
diff --git a/Tesses.WebServer.FileServer/Program.cs b/Tesses.WebServer.FileServer/Program.cs
index 32f7e71..4b780d7 100644
--- a/Tesses.WebServer.FileServer/Program.cs
+++ b/Tesses.WebServer.FileServer/Program.cs
@@ -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();
}
diff --git a/Tesses.WebServer.NetStandard/ServerContext.cs b/Tesses.WebServer.NetStandard/ServerContext.cs
index 67f746e..b6bd0d8 100644
--- a/Tesses.WebServer.NetStandard/ServerContext.cs
+++ b/Tesses.WebServer.NetStandard/ServerContext.cs
@@ -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();
+ }
///
/// Some user data
///
@@ -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;
}
diff --git a/Tesses.WebServer.NetStandard/SimpleHttpCode.cs b/Tesses.WebServer.NetStandard/SimpleHttpCode.cs
index da0d5c2..f6199c6 100644
--- a/Tesses.WebServer.NetStandard/SimpleHttpCode.cs
+++ b/Tesses.WebServer.NetStandard/SimpleHttpCode.cs
@@ -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
}
}
}
+ ///
+ /// Send file to client (supports range partial content)
+ ///
+ /// ServerContext
+ /// the file to serve
+
+ 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 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;
+ }
+ }
+ ///
+ /// Send file to client (supports range partial content)
+ ///
+ /// ServerContext
+ /// the file to serve
+
+ 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> 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();
+ var files = new Dictionary>();
var inputStream = new BufferedStream(serverCtx.GetRequestStream());
//Print("Before ParseUntillBoundaryEnd");
@@ -362,7 +476,7 @@ namespace Tesses.WebServer
}
///
- /// 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.
///
/// HTTP request.
/// Key-value pairs populated by the form data by this function.
@@ -371,7 +485,7 @@ namespace Tesses.WebServer
/// By default, is used, but for large files, it is recommended to open directly.
///
/// Name-file pair collection.
- public static Dictionary ParseBody(this ServerContext request, OnFile onFile)
+ public static Dictionary> 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 content_type = request.RequestHeaders.GetFirst("Content-Type");
if (content_type.StartsWith("application/x-www-form-urlencoded",StringComparison.Ordinal))
{
ParseForm(request);
+ var files = new Dictionary>();
+ 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.");
+
+ }
+ ///
+ /// Parses body of the request including form and multi-part form data.
+ ///
+ /// HTTP request.
+ /// Key-value pairs populated by the form data by this function.
+ ///
+ /// Function called if a file is about to be parsed. The stream is attached to a corresponding .
+ /// By default, is used, but for large files, it is recommended to open directly.
+ ///
+ /// Name-file pair collection.
+ public static Dictionary ParseBody(this ServerContext request, OnFile onFile)
+ {
+ var res=ParseBodyMultiple(request,onFile);
+ Dictionary files = new Dictionary();
+ foreach(var item in res)
+ {
+ if(item.Value.Count > 0)
+ {
+ files.Add(item.Key,item.Value[0]);
+ }
+ }
return files;
}
+ ///
+ /// 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.
+ ///
+ /// HTTP request.
+ /// The root directory to store all the uploads in
+ /// A HttpFileResponse Containing Paths to files, Dispose only deletes the files, so if you want to keep the files don't dispose it.
+ 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 responseEntries=new List();
+ 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 entries)
+ {
+ Directory = dir;
+ Files = entries;
+ }
+ public IReadOnlyList Files {get;}
+ public string Directory {get;}
+
+ public void Dispose()
+ {
+ System.IO.Directory.Delete(Directory,true);
+ }
}
///
diff --git a/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj b/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj
index 4642ce6..61b9c2e 100644
--- a/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj
+++ b/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj
@@ -5,14 +5,14 @@
Tesses.WebServer
Mike Nolan
Tesses
- 1.0.4.1
- 1.0.4.1
- 1.0.4.1
+ 1.0.4.2
+ 1.0.4.2
+ 1.0.4.2
A TCP Listener HTTP(s) Server
GPL-3.0-only
true
HTTP, WebServer, Website
- https://gitlab.tesses.cf/tesses50/tesses.webserver
+ https://gitlab.tesses.net/tesses50/tesses.webserver
diff --git a/Tesses.WebServer.NetStandard/TessesServer.cs b/Tesses.WebServer.NetStandard/TessesServer.cs
index 7120e23..277831c 100644
--- a/Tesses.WebServer.NetStandard/TessesServer.cs
+++ b/Tesses.WebServer.NetStandard/TessesServer.cs
@@ -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($"File {url} not found404 Not Found
{url}
");
+ }
+ public static async Task SendNotFoundAsync(this ServerContext ctx)
+ {
+ ctx.StatusCode=404;
+ string url=WebUtility.HtmlEncode(ctx.OriginalUrlPath);
+ await ctx.SendTextAsync($"File {url} not found404 Not Found
{url}
");
+ }
+ 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 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();
+
+ }
///
/// Read string from request body
///
@@ -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;
}
-
+
///
/// Write headers to stream
///
@@ -255,32 +331,7 @@ namespace Tesses.WebServer
var data = Encoding.UTF8.GetBytes(b.ToString());
ctx.NetworkStream.Write(data, 0, data.Length);
}
- ///
- /// Send file to client (supports range partial content)
- ///
- /// ServerContext
- /// the file to serve
-
- public static async Task SendFileAsync(this ServerContext ctx, string file)
- {
- using (var strm = File.OpenRead(file))
- {
- await ctx.SendStreamAsync( strm, MimeTypesMap.GetMimeType(file));
- }
- }
- ///
- /// Send file to client (supports range partial content)
- ///
- /// ServerContext
- /// the file to serve
-
- public static void SendFile(this ServerContext ctx, string file)
- {
- using (var strm = File.OpenRead(file))
- {
- ctx.SendStream( strm, MimeTypesMap.GetMimeType(file));
- }
- }
+
///
/// Send exception to client
///
@@ -290,7 +341,7 @@ namespace Tesses.WebServer
public static async Task SendExceptionAsync(this ServerContext ctx, Exception ex)
{
string name = ex.GetType().FullName;
- string j = $"{WebUtility.HtmlEncode(name)} thrown{WebUtility.HtmlEncode(name)} thrown
Description: {WebUtility.HtmlEncode(ex.Message)}
";
+ string j = $"{WebUtility.HtmlEncode(name)} thrown{WebUtility.HtmlEncode(name)} thrown
Description: {WebUtility.HtmlEncode(ex.ToString())}
";
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 = $"{WebUtility.HtmlEncode(name)} thrown{WebUtility.HtmlEncode(name)} thrown
Description: {WebUtility.HtmlEncode(ex.Message)}
";
+ string j = $"{WebUtility.HtmlEncode(name)} thrown{WebUtility.HtmlEncode(name)} thrown
Description: {WebUtility.HtmlEncode(ex.ToString())}
";
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 strs=new List();
+ 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("File {url} not accessable403 Forbidden
{url}
",403);
}
+ public bool RedirectToRootInsteadOfNotFound {get;set;}=false;
///
/// construct with path and with option to allow/deny listing directories
///
/// directory for server
/// 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
}
}
///
+ /// use values inside path like this /hello/YOUR_VALUE/somepage
+ ///
+ public class PathValueServer : Server
+ {
+ public IServer Server {get;set;}
+ int paths;
+ ///
+ /// Construct a PathValueServer with NotFoundServer
+ ///
+ /// How many path parts do you want, eg if your path is /jim/john/joel, 1: jim 2: jim/john 3: jim/john/joel
+ public PathValueServer(int paths=1)
+ {
+ Server = new NotFoundServer();
+ this.paths=paths;
+ }
+ ///
+ /// Construct a PathValueServer with your server
+ ///
+ /// Your server
+ /// How many path parts do you want, eg if your path is /jim/john/joel, 1: jim 2: jim/john 3: jim/john/joel
+ public PathValueServer(IServer inner,int paths=1)
+ {
+
+ Server = inner;
+ this.paths = paths;
+ }
+ ///
+ /// Use this inside your inner server to get string value
+ ///
+ /// the ServerContext
+ /// the string value from the url part
+ 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 kvps=new Dictionary();
+ 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 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;
+ }
+ }
+ ///
/// Abstract class for server
///
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);
+ }
+ }
}
///
/// 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
{
///
@@ -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();