Compare commits
10
Commits
7f44596e22
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a16846d7d | ||
|
|
d07cd21b98 | ||
|
|
ee57c7a3ca | ||
|
|
5e557faaaf | ||
|
|
e75b2048c9 | ||
|
|
fe1911c3cc | ||
|
|
565dd7cce3 | ||
|
|
78db763594 | ||
|
|
811e6cf7d6 | ||
|
|
358fb10348 |
@@ -2,27 +2,54 @@
|
|||||||
[](https://www.nuget.org/packages/Tesses.WebServer/)
|
[](https://www.nuget.org/packages/Tesses.WebServer/)
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
Starting with 1.0.3.9 this library will use GPL-3.0
|
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
|
If you can not use GPL either use 1.0.3.8 or use another library
|
||||||
<br>
|
|
||||||
A TcpListener HTTP Server
|
A TcpListener HTTP Server
|
||||||
|
|
||||||
Currently Supports
|
> 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
|
- GET+HEAD+POST Requests
|
||||||
- Seekable Video Files (Using Range)
|
- Seekable Video Files (or any file) (Using Range)
|
||||||
- Can Send Json To Client with helper function (uses Newtonsoft.Json)
|
- Can Send/Receive Json To/From Client with helper functions (uses Newtonsoft.Json)
|
||||||
- Cors Header
|
- 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
|
# 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)
|
- 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
|
- 404 Not Found Class
|
||||||
- Mount class (So you could use Multiple Apis, And Static Sites If you want)
|
- Mount class (So you could use Multiple Apis, And Static Sites If you want)
|
||||||
- Basic Auth Class
|
- Basic Auth Class
|
||||||
- Route Class (Just like dajuric/simple-http)
|
- 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.cf, 192.168.0.142, demilovato.com, ebay.com)
|
- 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
|
# 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"))
|
> 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"))
|
||||||
|
|
||||||
|
> Note the nuget icon is from [here](https://uxwing.com/http-icon/)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -1,12 +1,69 @@
|
|||||||
using Tesses;
|
using System.Drawing;
|
||||||
|
using Tesses;
|
||||||
using Tesses.WebServer;
|
using Tesses.WebServer;
|
||||||
|
using Tesses.WebServer.HtmlLayout;
|
||||||
namespace Tesses.WebServer.ConsoleApp
|
namespace Tesses.WebServer.ConsoleApp
|
||||||
{
|
{
|
||||||
|
class JsonObj
|
||||||
|
{
|
||||||
|
public string Name {get;set;}="";
|
||||||
|
|
||||||
|
public DateTime Birthday {get;set;}=DateTime.Now;
|
||||||
|
}
|
||||||
|
public class MyOther
|
||||||
|
{
|
||||||
|
[FormNewLine]
|
||||||
|
|
||||||
|
public string Hello {get;set;}="";
|
||||||
|
[FormNewLine]
|
||||||
|
|
||||||
|
public Color FavoriteColor {get;set;}=Color.Pink;
|
||||||
|
// [FormNewLine]
|
||||||
|
|
||||||
|
//public HttpFileResponseEntry[] Files {get;set;}=new HttpFileResponseEntry[0];
|
||||||
|
}
|
||||||
|
public enum TestEnum
|
||||||
|
{
|
||||||
|
Apple,
|
||||||
|
Orange,
|
||||||
|
Grape,
|
||||||
|
Banana,
|
||||||
|
Raspberry,
|
||||||
|
|
||||||
|
Blueberry,
|
||||||
|
Strawberry
|
||||||
|
}
|
||||||
|
class Test
|
||||||
|
{
|
||||||
|
[FormNewLine]
|
||||||
|
[FormText("Your Name",Placeholder="Name",Name="name")]
|
||||||
|
public string Name {get;set;}="";
|
||||||
|
[FormNewLine]
|
||||||
|
[FormText("Describe yourself",Placeholder="Description",Name="description")]
|
||||||
|
public string Description {get;set;}="";
|
||||||
|
[FormNewLine]
|
||||||
|
[FormCheckbox("Are you an adult")]
|
||||||
|
public bool Adult {get;set;}=false;
|
||||||
|
[FormNewLine]
|
||||||
|
[FormCheckbox("Email Me")]
|
||||||
|
public bool EmailMe {get;set;}=true;
|
||||||
|
|
||||||
|
[FormNewLine]
|
||||||
|
[FormRadio("fruit")]
|
||||||
|
public TestEnum Fruit {get;set;}=TestEnum.Raspberry;
|
||||||
|
|
||||||
|
[FormNewLine]
|
||||||
|
[FormFieldSet]
|
||||||
|
public MyOther MyOther {get;set;}=new MyOther();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
class MainClass
|
class MainClass
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
|
||||||
TestObject some_object = new TestObject();
|
TestObject some_object = new TestObject();
|
||||||
RouteServer rserver = new RouteServer();
|
RouteServer rserver = new RouteServer();
|
||||||
rserver.Add("/", async(ctx) => {
|
rserver.Add("/", async(ctx) => {
|
||||||
@@ -15,6 +72,64 @@ namespace Tesses.WebServer.ConsoleApp
|
|||||||
rserver.Add("/page", async(ctx) => {
|
rserver.Add("/page", async(ctx) => {
|
||||||
await ctx.SendTextAsync("Demetria Devonne Lovato 8/20/1992");
|
await ctx.SendTextAsync("Demetria Devonne Lovato 8/20/1992");
|
||||||
});
|
});
|
||||||
|
rserver.Add("/john",async(ctx)=>{
|
||||||
|
Test other = new Test();
|
||||||
|
ctx.ParseSmartForm(other);
|
||||||
|
|
||||||
|
await ctx.SendJsonAsync(other);
|
||||||
|
},"POST");
|
||||||
|
rserver.Add("/html_ex",async(ctx)=>{await ctx.SendHtmlAsync(H.Html(
|
||||||
|
H.Head(
|
||||||
|
H.Meta().WithAttribute("charset","UTF-8"),
|
||||||
|
H.Meta().WithAttribute("name","viewport").WithAttribute("content","width=device-width, initial-scale=1.0"),
|
||||||
|
H.Title("Document")
|
||||||
|
),
|
||||||
|
H.Body(H.Form("./john",new Test(),true))
|
||||||
|
).WithAttribute("lang","en"));});
|
||||||
|
rserver.Add("/absolute_paths",(ctx)=>{
|
||||||
|
using(var sw = ctx.GetResponseStreamWriter())
|
||||||
|
{
|
||||||
|
sw.WriteLine($"Root: {ctx.GetRealRootUrl()}");
|
||||||
|
sw.WriteLine($"Current Server Root: {ctx.GetCurrentServerPath()}");
|
||||||
|
sw.WriteLine($"Demetria: {ctx.GetRealUrlRelativeToCurrentServer("/page")}");
|
||||||
|
sw.WriteLine($"Headers: {ctx.GetRealUrlRelativeToCurrentServer("/headers")}");
|
||||||
|
sw.WriteLine($"Relative To Root: {ctx.GetRealUrl("/johnconnor/")}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rserver.Add("/headers",(ctx)=>{
|
||||||
|
using(var sw = ctx.GetResponseStreamWriter())
|
||||||
|
{
|
||||||
|
foreach(var item in ctx.RequestHeaders)
|
||||||
|
{
|
||||||
|
foreach(var item2 in item.Value)
|
||||||
|
{
|
||||||
|
sw.WriteLine($"{item.Key}: {item2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
var ip=System.Net.IPAddress.Any;
|
||||||
StaticServer static_server = new StaticServer(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyVideos));
|
StaticServer static_server = new StaticServer(System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyVideos));
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Tesses.WebServer.NetStandard\Tesses.WebServer.NetStandard.csproj" />
|
<ProjectReference Include="..\Tesses.WebServer.NetStandard\Tesses.WebServer.NetStandard.csproj" />
|
||||||
|
<ProjectReference Include="..\Tesses.WebServer.HtmlLayout\Tesses.WebServer.HtmlLayout.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Tesses;
|
||||||
|
using Tesses.WebServer;
|
||||||
|
using Tesses.VirtualFilesystem;
|
||||||
|
using Tesses.VirtualFilesystem.Filesystems;
|
||||||
|
namespace Tesses.WebServer.ConsoleApp
|
||||||
|
{
|
||||||
|
|
||||||
|
class MainClass
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
|
||||||
|
var ip=System.Net.IPAddress.Any;
|
||||||
|
var lfs=new LocalFileSystem().GetSubdirFilesystem(Special.CurDir / "files");
|
||||||
|
|
||||||
|
StaticServer static_server = new StaticServer(lfs,true);
|
||||||
|
static_server.AllowUpload=true;
|
||||||
|
static_server.RedirectToRootInsteadOfNotFound = true;
|
||||||
|
|
||||||
|
HttpServerListener s = new HttpServerListener(new System.Net.IPEndPoint(ip, 24240),static_server);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
s.ListenAsync(System.Threading.CancellationToken.None).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Tesses.WebServer.NetStandard\Tesses.WebServer.NetStandard.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Tesses.VirtualFilesystem.Local" Version="1.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Hi
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System;
|
using System;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace Tesses.WebServer
|
namespace Tesses.WebServer
|
||||||
{
|
{
|
||||||
@@ -70,6 +72,31 @@ internal class SizedStream : Stream
|
|||||||
}
|
}
|
||||||
public class ServerContext
|
public class ServerContext
|
||||||
{
|
{
|
||||||
|
static Mutex mtx=new Mutex();
|
||||||
|
static long _unique=0;
|
||||||
|
|
||||||
|
public static long UniqueNumber()
|
||||||
|
{
|
||||||
|
mtx.WaitOne();
|
||||||
|
long u=_unique++;
|
||||||
|
mtx.ReleaseMutex();
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
/// <summary>
|
||||||
/// Some user data
|
/// Some user data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -277,6 +304,10 @@ internal class SizedStream : Stream
|
|||||||
return new SizedStream(NetworkStream,len);
|
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");
|
//DajuricSimpleHttpExtensions.Print("Returns NetworkStream");
|
||||||
return NetworkStream;
|
return NetworkStream;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ using System.Linq;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using HeyRed.Mime;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Tesses.WebServer
|
namespace Tesses.WebServer
|
||||||
{
|
{
|
||||||
@@ -82,6 +85,7 @@ namespace Tesses.WebServer
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var strm2=new ChunkedStream(ctx.NetworkStream,false);
|
||||||
long tread=0;
|
long tread=0;
|
||||||
byte[] buffer=new byte[8*1024*1024];
|
byte[] buffer=new byte[8*1024*1024];
|
||||||
int read=0;
|
int read=0;
|
||||||
@@ -94,7 +98,7 @@ namespace Tesses.WebServer
|
|||||||
}
|
}
|
||||||
if(read == 0) break;
|
if(read == 0) break;
|
||||||
read = strm.Read(buffer,0,read);
|
read = strm.Read(buffer,0,read);
|
||||||
strm.Write(buffer,0,read);
|
strm2.Write(buffer,0,read);
|
||||||
}while(read > 0);
|
}while(read > 0);
|
||||||
} finally {
|
} finally {
|
||||||
strm.Close();
|
strm.Close();
|
||||||
@@ -105,6 +109,7 @@ namespace Tesses.WebServer
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var strm2=new ChunkedStream(ctx.NetworkStream,false);
|
||||||
long tread=0;
|
long tread=0;
|
||||||
byte[] buffer=new byte[8*1024*1024];
|
byte[] buffer=new byte[8*1024*1024];
|
||||||
int read=0;
|
int read=0;
|
||||||
@@ -117,7 +122,7 @@ namespace Tesses.WebServer
|
|||||||
}
|
}
|
||||||
if(read == 0) break;
|
if(read == 0) break;
|
||||||
read = await strm.ReadAsync(buffer,0,read);
|
read = await strm.ReadAsync(buffer,0,read);
|
||||||
await strm.WriteAsync(buffer,0,read);
|
await strm2.WriteAsync(buffer,0,read);
|
||||||
}while(read > 0);
|
}while(read > 0);
|
||||||
} finally {
|
} finally {
|
||||||
strm.Close();
|
strm.Close();
|
||||||
@@ -151,7 +156,8 @@ namespace Tesses.WebServer
|
|||||||
|
|
||||||
}
|
}
|
||||||
ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString());
|
ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString());
|
||||||
ctx.ResponseHeaders.Add("Content-Type", contentType);
|
|
||||||
|
ctx.WithMimeType(contentType);
|
||||||
|
|
||||||
ctx.WriteHeaders();
|
ctx.WriteHeaders();
|
||||||
if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal))
|
if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal))
|
||||||
@@ -196,8 +202,7 @@ namespace Tesses.WebServer
|
|||||||
|
|
||||||
}
|
}
|
||||||
ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString());
|
ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString());
|
||||||
ctx.ResponseHeaders.Add("Content-Type", contentType);
|
ctx.WithMimeType(contentType);
|
||||||
|
|
||||||
await ctx.WriteHeadersAsync();
|
await ctx.WriteHeadersAsync();
|
||||||
if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal))
|
if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
@@ -214,8 +219,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>
|
||||||
|
|
||||||
static Dictionary<string, HttpFile> ParseMultipartForm(ServerContext serverCtx, OnFile onFile)
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
var args = serverCtx.QueryParams;
|
||||||
string content_type=serverCtx.RequestHeaders.GetFirst("Content-Type");
|
string content_type=serverCtx.RequestHeaders.GetFirst("Content-Type");
|
||||||
@@ -226,7 +341,7 @@ namespace Tesses.WebServer
|
|||||||
boundary = "--" + boundary;
|
boundary = "--" + boundary;
|
||||||
|
|
||||||
|
|
||||||
var files = new Dictionary<string, HttpFile>();
|
var files = new Dictionary<string, List<HttpFile>>();
|
||||||
var inputStream = new BufferedStream(serverCtx.GetRequestStream());
|
var inputStream = new BufferedStream(serverCtx.GetRequestStream());
|
||||||
//Print("Before ParseUntillBoundaryEnd");
|
//Print("Before ParseUntillBoundaryEnd");
|
||||||
|
|
||||||
@@ -362,7 +477,7 @@ namespace Tesses.WebServer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="request">HTTP request.</param>
|
/// <param name="request">HTTP request.</param>
|
||||||
/// <param name="args">Key-value pairs populated by the form data by this function.</param>
|
/// <param name="args">Key-value pairs populated by the form data by this function.</param>
|
||||||
@@ -371,7 +486,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>
|
/// <para>By default, <see cref="MemoryStream"/> is used, but for large files, it is recommended to open <see cref="FileStream"/> directly.</para>
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <returns>Name-file pair collection.</returns>
|
/// <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)
|
if (request == null)
|
||||||
throw new ArgumentNullException(nameof(request));
|
throw new ArgumentNullException(nameof(request));
|
||||||
@@ -383,21 +498,133 @@ namespace Tesses.WebServer
|
|||||||
throw new ArgumentNullException(nameof(onFile));
|
throw new ArgumentNullException(nameof(onFile));
|
||||||
|
|
||||||
|
|
||||||
var files = new Dictionary<string, HttpFile>();
|
|
||||||
string content_type = request.RequestHeaders.GetFirst("Content-Type");
|
string content_type = request.RequestHeaders.GetFirst("Content-Type");
|
||||||
if (content_type.StartsWith("application/x-www-form-urlencoded",StringComparison.Ordinal))
|
if (content_type.StartsWith("application/x-www-form-urlencoded",StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
ParseForm(request);
|
ParseForm(request);
|
||||||
|
var files = new Dictionary<string, List<HttpFile>>();
|
||||||
|
return files;
|
||||||
}
|
}
|
||||||
else if (content_type.StartsWith("multipart/form-data",StringComparison.Ordinal))
|
else if (content_type.StartsWith("multipart/form-data",StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
files = ParseMultipartForm(request, onFile);
|
return ParseMultipartForm(request, onFile);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw new NotSupportedException("The body content-type is not supported.");
|
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;
|
return files;
|
||||||
}
|
}
|
||||||
|
public static HttpFileResponse ParseBodyWithTempDirectory(this ServerContext request)
|
||||||
|
{
|
||||||
|
DateTime dt=DateTime.Now;
|
||||||
|
return request.ParseBodyWithTempDirectory(Path.Combine(Path.GetTempPath(),$"TWSUPLOAD_{dt.ToString("yyyyMMdd_HHmmss")}_{ServerContext.UniqueNumber()}"));
|
||||||
|
}
|
||||||
|
/// <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;}
|
||||||
|
[JsonIgnore]
|
||||||
|
public FileInfo FileInfo => new FileInfo(Path);
|
||||||
|
[JsonIgnore]
|
||||||
|
public object PrivateData {get;set;}=null;
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
if(System.IO.Directory.Exists(Directory))
|
||||||
|
System.IO.Directory.Delete(Directory,true);
|
||||||
|
}
|
||||||
|
~HttpFileResponse()
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -485,25 +712,47 @@ namespace Tesses.WebServer
|
|||||||
public delegate void HttpAction(ServerContext ctx);
|
public delegate void HttpAction(ServerContext ctx);
|
||||||
public class RouteServer : Server
|
public class RouteServer : Server
|
||||||
{
|
{
|
||||||
|
public RouteServer() : this(new NotFoundServer())
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
public RouteServer(IServer otherServer)
|
||||||
|
{
|
||||||
|
this.otherServer=otherServer;
|
||||||
|
}
|
||||||
public List<(ShouldProcessFunc ShouldProcessFunc, HttpActionAsync Action)> Methods = new List<(ShouldProcessFunc ShouldProcessFunc, HttpActionAsync Action)>();
|
public List<(ShouldProcessFunc ShouldProcessFunc, HttpActionAsync Action)> Methods = new List<(ShouldProcessFunc ShouldProcessFunc, HttpActionAsync Action)>();
|
||||||
|
private IServer otherServer;
|
||||||
|
|
||||||
public override async Task GetAsync(ServerContext ctx)
|
public override async Task GetAsync(ServerContext ctx)
|
||||||
{
|
{
|
||||||
await Process(ctx);
|
if(!await Process(ctx))
|
||||||
|
{
|
||||||
|
await Guaranteed(otherServer).GetAsync(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public override async Task PostAsync(ServerContext ctx)
|
public override async Task PostAsync(ServerContext ctx)
|
||||||
{
|
{
|
||||||
await Process(ctx);
|
if(!await Process(ctx))
|
||||||
|
{
|
||||||
|
await Guaranteed(otherServer).PostAsync(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public override async Task OtherAsync(ServerContext ctx)
|
public override async Task OtherAsync(ServerContext ctx)
|
||||||
{
|
{
|
||||||
await Process(ctx);
|
if(!await Process(ctx))
|
||||||
|
{
|
||||||
|
await Guaranteed(otherServer).OtherAsync(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OptionsAsync(ServerContext ctx)
|
public override async Task OptionsAsync(ServerContext ctx)
|
||||||
{
|
{
|
||||||
await Process(ctx);
|
if(!await Process(ctx))
|
||||||
|
{
|
||||||
|
await Guaranteed(otherServer).OptionsAsync(ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private async Task Process(ServerContext ctx)
|
private async Task<bool> Process(ServerContext ctx)
|
||||||
{
|
{
|
||||||
foreach(var (shouldProcessFunc,action) in Methods)
|
foreach(var (shouldProcessFunc,action) in Methods)
|
||||||
{
|
{
|
||||||
@@ -514,8 +763,9 @@ namespace Tesses.WebServer
|
|||||||
}
|
}
|
||||||
|
|
||||||
await action(ctx);
|
await action(ctx);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds the specified action to the route collection.
|
/// Adds the specified action to the route collection.
|
||||||
|
|||||||
@@ -5,14 +5,16 @@
|
|||||||
<PackageId>Tesses.WebServer</PackageId>
|
<PackageId>Tesses.WebServer</PackageId>
|
||||||
<Author>Mike Nolan</Author>
|
<Author>Mike Nolan</Author>
|
||||||
<Company>Tesses</Company>
|
<Company>Tesses</Company>
|
||||||
<Version>1.0.3.9</Version>
|
<Version>1.0.4.3</Version>
|
||||||
<AssemblyVersion>1.0.3.9</AssemblyVersion>
|
<AssemblyVersion>1.0.4.3</AssemblyVersion>
|
||||||
<FileVersion>1.0.3.9</FileVersion>
|
<FileVersion>1.0.4.3</FileVersion>
|
||||||
<Description>A TCP Listener HTTP(s) Server</Description>
|
<Description>A TCP Listener HTTP(s) Server</Description>
|
||||||
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
|
||||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>HTTP, WebServer, Website</PackageTags>
|
<PackageTags>HTTP, WebServer, Website</PackageTags>
|
||||||
<RepositoryUrl>https://gitlab.tesses.cf/tesses50/tesses.webserver</RepositoryUrl>
|
<RepositoryUrl>https://gitea.site.tesses.net/tesses50/tesses.webserver</RepositoryUrl>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<PackageIcon>http-icon.png</PackageIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -20,5 +22,8 @@
|
|||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="Tesses.VirtualFileSystem.Base" Version="1.0.0" />
|
<PackageReference Include="Tesses.VirtualFileSystem.Base" Version="1.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="..\http-icon.png" Pack="true" PackagePath="\" />
|
||||||
|
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ using System.Net.Security;
|
|||||||
using System.Security.Authentication;
|
using System.Security.Authentication;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using Tesses.VirtualFilesystem;
|
using Tesses.VirtualFilesystem;
|
||||||
|
using System.Net.Mime;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
namespace Tesses.WebServer
|
namespace Tesses.WebServer
|
||||||
{
|
{
|
||||||
@@ -42,6 +44,93 @@ namespace Tesses.WebServer
|
|||||||
|
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
|
public static string GetRealRootUrl(this ServerContext ctx)
|
||||||
|
{
|
||||||
|
if(ctx.RequestHeaders.TryGetFirst("X-Forwarded-Path",out var xfwp))
|
||||||
|
{
|
||||||
|
return $"{xfwp.TrimEnd('/')}/";
|
||||||
|
}
|
||||||
|
else if(ctx.RequestHeaders.TryGetFirst("X-Forwarded-Host",out var host))
|
||||||
|
{
|
||||||
|
if(!ctx.RequestHeaders.TryGetFirst("X-Forwarded-Proto",out var proto)) proto="http";
|
||||||
|
return $"{proto}://{host}/";
|
||||||
|
}
|
||||||
|
else if(ctx.RequestHeaders.TryGetFirst("Host",out var theHost))
|
||||||
|
{
|
||||||
|
return $"http://{theHost}/";
|
||||||
|
}
|
||||||
|
return $"http://{ctx.Client}/";
|
||||||
|
}
|
||||||
|
public static string GetRealUrl(this ServerContext ctx,string url)
|
||||||
|
{
|
||||||
|
return $"{ctx.GetRealRootUrl()}{url.TrimStart('/')}";
|
||||||
|
}
|
||||||
|
public static string GetCurrentServerPath(this ServerContext ctx)
|
||||||
|
{
|
||||||
|
if(ctx.UrlPath == ctx.OriginalUrlPath) return "/";
|
||||||
|
return $"{ctx.OriginalUrlPath.Remove(ctx.OriginalUrlPath.Length-ctx.UrlPath.Length).TrimEnd('/')}/";
|
||||||
|
}
|
||||||
|
public static string GetRealUrlRelativeToCurrentServer(this ServerContext ctx, string url)
|
||||||
|
{
|
||||||
|
return ctx.GetRealUrl($"{ctx.GetCurrentServerPath()}{url.TrimStart('/')}");
|
||||||
|
}
|
||||||
|
public static ServerContext WithStatusCode(this ServerContext ctx, int statusCode)
|
||||||
|
{
|
||||||
|
ctx.StatusCode = statusCode;
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
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)
|
public static async Task WriteAsync(this Stream strm,string text)
|
||||||
{
|
{
|
||||||
var data=Encoding.UTF8.GetBytes(text);
|
var data=Encoding.UTF8.GetBytes(text);
|
||||||
@@ -63,7 +152,11 @@ namespace Tesses.WebServer
|
|||||||
try{
|
try{
|
||||||
EventHandler<SendEventArgs> cb= (sender,e0)=>{
|
EventHandler<SendEventArgs> cb= (sender,e0)=>{
|
||||||
if(__connected)
|
if(__connected)
|
||||||
ctx.NetworkStream.Write($"data: {e0.Data}\n\n");
|
{
|
||||||
|
ctx.NetworkStream.Write($"data: {e0.Data}\n\n");
|
||||||
|
ctx.NetworkStream.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
evt.EventReceived += cb;
|
evt.EventReceived += cb;
|
||||||
while(ctx.Connected);
|
while(ctx.Connected);
|
||||||
@@ -82,13 +175,36 @@ namespace Tesses.WebServer
|
|||||||
public static async Task<string> ReadStringAsync(this ServerContext ctx)
|
public static async Task<string> ReadStringAsync(this ServerContext ctx)
|
||||||
{
|
{
|
||||||
string str = null;
|
string str = null;
|
||||||
using (var reader = new StreamReader(ctx.GetRequestStream()))
|
using (var reader = ctx.GetRequestStreamReader())
|
||||||
{
|
{
|
||||||
str = await reader.ReadToEndAsync();
|
str = await reader.ReadToEndAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
return str;
|
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>
|
/// <summary>
|
||||||
/// Read string from request body
|
/// Read string from request body
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -97,7 +213,7 @@ namespace Tesses.WebServer
|
|||||||
public static string ReadString(this ServerContext ctx)
|
public static string ReadString(this ServerContext ctx)
|
||||||
{
|
{
|
||||||
string str = null;
|
string str = null;
|
||||||
using (var reader = new StreamReader(ctx.GetRequestStream()))
|
using (var reader = ctx.GetRequestStreamReader())
|
||||||
{
|
{
|
||||||
str = reader.ReadToEnd();
|
str = reader.ReadToEnd();
|
||||||
}
|
}
|
||||||
@@ -255,32 +371,7 @@ namespace Tesses.WebServer
|
|||||||
var data = Encoding.UTF8.GetBytes(b.ToString());
|
var data = Encoding.UTF8.GetBytes(b.ToString());
|
||||||
ctx.NetworkStream.Write(data, 0, data.Length);
|
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>
|
/// <summary>
|
||||||
/// Send exception to client
|
/// Send exception to client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -290,7 +381,7 @@ namespace Tesses.WebServer
|
|||||||
public static async Task SendExceptionAsync(this ServerContext ctx, Exception ex)
|
public static async Task SendExceptionAsync(this ServerContext ctx, Exception ex)
|
||||||
{
|
{
|
||||||
string name = ex.GetType().FullName;
|
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.StatusCode = 500;
|
||||||
await ctx.SendTextAsync(j);
|
await ctx.SendTextAsync(j);
|
||||||
}
|
}
|
||||||
@@ -303,7 +394,7 @@ namespace Tesses.WebServer
|
|||||||
public static void SendException(this ServerContext ctx, Exception ex)
|
public static void SendException(this ServerContext ctx, Exception ex)
|
||||||
{
|
{
|
||||||
string name = ex.GetType().FullName;
|
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.StatusCode = 500;
|
||||||
ctx.SendText(j);
|
ctx.SendText(j);
|
||||||
}
|
}
|
||||||
@@ -432,6 +523,229 @@ namespace Tesses.WebServer
|
|||||||
value = default(T2);
|
value = default(T2);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, object value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, int value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, short value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, long value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, Guid value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, byte value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, sbyte value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, uint value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, ushort value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, ulong value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, bool value)
|
||||||
|
{
|
||||||
|
args.Add(key,value ? "true" : "false");
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, float value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, double value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
public static void Add<T1>(this Dictionary<T1,List<string>> args,T1 key, decimal value)
|
||||||
|
{
|
||||||
|
args.Add(key,value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetFirstInt64<T1>(this Dictionary<T1,List<string>> args,T1 key, out long value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= long.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool TryGetFirstInt32<T1>(this Dictionary<T1,List<string>> args,T1 key, out int value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= int.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool TryGetFirstInt16<T1>(this Dictionary<T1,List<string>> args,T1 key, out short value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= short.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool TryGetFirstInt8<T1>(this Dictionary<T1,List<string>> args,T1 key, out sbyte value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= sbyte.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool GetFirstBoolean<T1>(this Dictionary<T1,List<string>> args,T1 key)
|
||||||
|
{
|
||||||
|
if(args.TryGetFirst(key,out var value))
|
||||||
|
{
|
||||||
|
value=value.ToLower();
|
||||||
|
if(value == "off" || value == "no" || value == "false" || value == "0") return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool TryGetFirstGuid<T1>(this Dictionary<T1,List<string>> args,T1 key, out Guid value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= Guid.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=Guid.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool TryGetFirstUInt8<T1>(this Dictionary<T1,List<string>> args,T1 key, out byte value)
|
||||||
|
{
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= byte.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool TryGetFirstUInt16<T1>(this Dictionary<T1,List<string>> args,T1 key, out ushort value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= ushort.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool TryGetFirstUInt64<T1>(this Dictionary<T1,List<string>> args,T1 key, out ulong value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= ulong.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool TryGetFirstUInt32<T1>(this Dictionary<T1,List<string>> args,T1 key, out uint value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= uint.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetFirstFloat<T1>(this Dictionary<T1,List<string>> args,T1 key, out float value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= float.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0.0f;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool TryGetFirstBigInteger<T1>(this Dictionary<T1,List<string>> args,T1 key, out BigInteger value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= BigInteger.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool TryGetFirstDecimal<T1>(this Dictionary<T1,List<string>> args,T1 key, out decimal value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= decimal.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0.0M;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
public static bool TryGetFirstDouble<T1>(this Dictionary<T1,List<string>> args,T1 key, out double value)
|
||||||
|
{
|
||||||
|
|
||||||
|
if(args.TryGetFirst(key,out var str))
|
||||||
|
{
|
||||||
|
bool res= double.TryParse(str,out var val);
|
||||||
|
value = val;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
value=0.0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add item to the Dictionary<T1,List<T2>> with specified key (will create key in dictionary if not exist)
|
/// Add item to the Dictionary<T1,List<T2>> with specified key (will create key in dictionary if not exist)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -513,6 +827,8 @@ namespace Tesses.WebServer
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
public class SameServer : Server
|
public class SameServer : Server
|
||||||
{
|
{
|
||||||
@@ -534,6 +850,7 @@ namespace Tesses.WebServer
|
|||||||
ctx.StatusCode = _statusCode;
|
ctx.StatusCode = _statusCode;
|
||||||
await ctx.SendTextAsync( _html.Replace("{url}", WebUtility.HtmlEncode(ctx.OriginalUrlPath)));
|
await ctx.SendTextAsync( _html.Replace("{url}", WebUtility.HtmlEncode(ctx.OriginalUrlPath)));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum WebServerPathType
|
public enum WebServerPathType
|
||||||
@@ -604,7 +921,7 @@ namespace Tesses.WebServer
|
|||||||
{
|
{
|
||||||
foreach(var def in _defaultFileNames)
|
foreach(var def in _defaultFileNames)
|
||||||
{
|
{
|
||||||
var _name = Special.Root / path;
|
var _name = Special.Root / path / def; //woops we need the def here
|
||||||
name=_name.Path;
|
name=_name.Path;
|
||||||
if(_fs.FileExists(_name))
|
if(_fs.FileExists(_name))
|
||||||
{
|
{
|
||||||
@@ -693,9 +1010,23 @@ namespace Tesses.WebServer
|
|||||||
name = "";
|
name = "";
|
||||||
return false;
|
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)
|
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);
|
//Console.WriteLine(someUrl);
|
||||||
if (Directory.Exists(someUrl))
|
if (Directory.Exists(someUrl))
|
||||||
{
|
{
|
||||||
@@ -967,6 +1298,7 @@ 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);
|
_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>
|
/// <summary>
|
||||||
/// construct with path and with option to allow/deny listing directories
|
/// construct with path and with option to allow/deny listing directories
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1070,7 +1402,35 @@ namespace Tesses.WebServer
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case WebServerPathType.NotFound:
|
case WebServerPathType.NotFound:
|
||||||
await _server.GetAsync(ctx);
|
if(RedirectToRootInsteadOfNotFound)
|
||||||
|
{
|
||||||
|
|
||||||
|
var fileEntry2 = fileHandler.GetPath("/");
|
||||||
|
switch(fileEntry2.Type)
|
||||||
|
{
|
||||||
|
case WebServerPathType.File:
|
||||||
|
using(var strm = fileHandler.Open(fileEntry2))
|
||||||
|
await ctx.SendStreamAsync(strm,HeyRed.Mime.MimeTypesMap.GetMimeType(fileEntry2.FileName));
|
||||||
|
break;
|
||||||
|
case WebServerPathType.Directory:
|
||||||
|
if(AllowListingDirectories)
|
||||||
|
{
|
||||||
|
DirectoryLister lister=new DirectoryLister();
|
||||||
|
lister.FromDirectory(fileHandler,"/",ctx.OriginalUrlPath);
|
||||||
|
await lister.GetAsync(ctx);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _forbidden.GetAsync(ctx);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case WebServerPathType.NotFound:
|
||||||
|
await _server.GetAsync(ctx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
await _server.GetAsync(ctx);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}catch(UnauthorizedAccessException ex)
|
}catch(UnauthorizedAccessException ex)
|
||||||
@@ -1212,6 +1572,124 @@ namespace Tesses.WebServer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <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
|
/// Abstract class for server
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class Server : IServer
|
public abstract class Server : IServer
|
||||||
@@ -1299,6 +1777,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>
|
/// <summary>
|
||||||
/// mount multiple servers at different url paths
|
/// mount multiple servers at different url paths
|
||||||
@@ -1309,6 +1817,10 @@ namespace Tesses.WebServer
|
|||||||
public MountableServer(IServer root)
|
public MountableServer(IServer root)
|
||||||
{
|
{
|
||||||
_root = root;
|
_root = root;
|
||||||
|
}
|
||||||
|
public MountableServer() : this(new NotFoundServer())
|
||||||
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
IServer _root;
|
IServer _root;
|
||||||
private (string Key,IServer Value) GetFromPath(ServerContext ctx)
|
private (string Key,IServer Value) GetFromPath(ServerContext ctx)
|
||||||
@@ -1620,6 +2132,140 @@ namespace Tesses.WebServer
|
|||||||
Task OtherAsync(ServerContext ctx);
|
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
|
public sealed class HttpServerListener
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -1829,8 +2475,7 @@ namespace Tesses.WebServer
|
|||||||
{
|
{
|
||||||
StringBuilder s = new StringBuilder();
|
StringBuilder s = new StringBuilder();
|
||||||
|
|
||||||
var decoder = Encoding.UTF8.GetDecoder();
|
|
||||||
var nextChar = new char[1];
|
|
||||||
while (!s.EndsWith("\r\n\r\n",StringComparison.Ordinal))
|
while (!s.EndsWith("\r\n\r\n",StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
int data = strm.ReadByte();
|
int data = strm.ReadByte();
|
||||||
@@ -1838,9 +2483,8 @@ namespace Tesses.WebServer
|
|||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
int charCount=decoder.GetChars(new byte[] { (byte)data }, 0, 1, nextChar, 0);
|
if(data > 127) continue;
|
||||||
if (charCount == 0) continue;
|
s.Append((char)data); //its ascii now
|
||||||
s.Append(nextChar);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.ToString();
|
return s.ToString();
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesses.WebServer.Console",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesses.WebServer.NetStandard", "Tesses.WebServer.NetStandard\Tesses.WebServer.NetStandard.csproj", "{C76507C5-D30C-46CB-A527-5CF7090CAED6}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesses.WebServer.NetStandard", "Tesses.WebServer.NetStandard\Tesses.WebServer.NetStandard.csproj", "{C76507C5-D30C-46CB-A527-5CF7090CAED6}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tesses.WebServer.FileServer", "Tesses.WebServer.FileServer\Tesses.WebServer.FileServer.csproj", "{50A4EEEE-1ACD-48C2-B454-77B7E1B6620A}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -24,5 +26,9 @@ Global
|
|||||||
{C76507C5-D30C-46CB-A527-5CF7090CAED6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C76507C5-D30C-46CB-A527-5CF7090CAED6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{C76507C5-D30C-46CB-A527-5CF7090CAED6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C76507C5-D30C-46CB-A527-5CF7090CAED6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C76507C5-D30C-46CB-A527-5CF7090CAED6}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C76507C5-D30C-46CB-A527-5CF7090CAED6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{50A4EEEE-1ACD-48C2-B454-77B7E1B6620A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{50A4EEEE-1ACD-48C2-B454-77B7E1B6620A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{50A4EEEE-1ACD-48C2-B454-77B7E1B6620A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{50A4EEEE-1ACD-48C2-B454-77B7E1B6620A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
Reference in New Issue
Block a user