2022-04-02 18:59:12 +00:00
using System ;
using System.Net ;
using System.Net.Sockets ;
using System.Threading.Tasks ;
using System.IO ;
using System.Threading ;
using System.Text ;
using System.Collections.Generic ;
using System.Linq ;
using HeyRed.Mime ;
using Newtonsoft.Json ;
2022-04-03 14:39:54 +00:00
using System.Security.Cryptography.X509Certificates ;
using System.Net.Security ;
2022-04-03 15:36:38 +00:00
using System.Security.Authentication ;
2022-12-14 18:08:52 +00:00
using System.Web ;
using Tesses.VirtualFilesystem ;
2024-03-18 12:31:51 +00:00
using System.Net.Mime ;
2024-03-31 01:01:24 +00:00
using System.Numerics ;
2022-04-02 18:59:12 +00:00
namespace Tesses.WebServer
{
2022-12-14 18:08:52 +00:00
internal class SendEventArgs : EventArgs
2022-09-01 05:26:26 +00:00
{
public string Data { get ; set ; }
}
public class SendEvents
{
internal event EventHandler < SendEventArgs > EventReceived ;
public void SendEvent ( object data )
{
SendEvent ( JsonConvert . SerializeObject ( data ) ) ;
}
public void SendEvent ( string e )
{
try {
EventReceived ? . Invoke ( this , new SendEventArgs ( ) { Data = e } ) ;
} catch ( Exception ex )
{
_ = ex ;
}
}
}
2022-04-02 21:15:20 +00:00
public static class Extensions
2022-09-01 05:26:26 +00:00
{
2024-03-31 01:01:24 +00:00
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 ;
}
2024-03-18 12:31:51 +00:00
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 ;
}
2022-09-01 05:26:26 +00:00
public static async Task WriteAsync ( this Stream strm , string text )
{
var data = Encoding . UTF8 . GetBytes ( text ) ;
await strm . WriteAsync ( data , 0 , data . Length ) ;
await strm . FlushAsync ( ) ;
}
public static void Write ( this Stream strm , string text )
{
var data = Encoding . UTF8 . GetBytes ( text ) ;
strm . Write ( data , 0 , data . Length ) ;
strm . Flush ( ) ;
}
public static void ServerSentEvents ( this ServerContext ctx , SendEvents evt )
{
bool __connected = true ;
ctx . ResponseHeaders . Add ( "Content-Type" , "text/event-stream" ) ;
ctx . ResponseHeaders . Add ( "Cache-Control" , "no-cache" ) ;
ctx . WriteHeaders ( ) ;
try {
EventHandler < SendEventArgs > cb = ( sender , e0 ) = > {
if ( __connected )
2024-03-31 01:01:24 +00:00
{
ctx . NetworkStream . Write ( $"data: {e0.Data}\n\n" ) ;
ctx . NetworkStream . Flush ( ) ;
}
2022-09-01 05:26:26 +00:00
} ;
evt . EventReceived + = cb ;
while ( ctx . Connected ) ;
evt . EventReceived - = cb ;
__connected = false ;
} catch ( Exception ex )
{
_ = ex ;
}
}
2022-04-24 18:56:06 +00:00
/// <summary>
/// Read string from request body
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <returns>the contents of request</returns>
public static async Task < string > ReadStringAsync ( this ServerContext ctx )
{
string str = null ;
2024-03-18 12:31:51 +00:00
using ( var reader = ctx . GetRequestStreamReader ( ) )
2022-04-24 18:56:06 +00:00
{
str = await reader . ReadToEndAsync ( ) ;
}
return str ;
}
2024-03-18 12:31:51 +00:00
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 ( ) ;
}
2022-04-24 18:56:06 +00:00
/// <summary>
2022-09-01 05:26:26 +00:00
/// Read string from request body
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <returns>the contents of request</returns>
public static string ReadString ( this ServerContext ctx )
{
string str = null ;
2024-03-18 12:31:51 +00:00
using ( var reader = ctx . GetRequestStreamReader ( ) )
2022-09-01 05:26:26 +00:00
{
str = reader . ReadToEnd ( ) ;
}
return str ;
}
/// <summary>
2022-04-24 18:56:06 +00:00
/// Read json from request body
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <typeparam name="T">type of object (for scema)</typeparam>
/// <returns>object of type T with deserialized json data</returns>
public static async Task < T > ReadJsonAsync < T > ( this ServerContext ctx )
{
var json = await ctx . ReadStringAsync ( ) ;
return JsonConvert . DeserializeObject < T > ( json ) ;
}
/// <summary>
2022-09-01 05:26:26 +00:00
/// Read json from request body
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <typeparam name="T">type of object (for scema)</typeparam>
/// <returns>object of type T with deserialized json data</returns>
public static T ReadJson < T > ( this ServerContext ctx )
{
var json = ctx . ReadString ( ) ;
return JsonConvert . DeserializeObject < T > ( json ) ;
}
/// <summary>
2022-04-24 18:56:06 +00:00
/// Read request body to array
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <returns>Request body data</returns>
public static async Task < byte [ ] > ReadBytesAsync ( this ServerContext ctx )
{
MemoryStream strm = new MemoryStream ( ) ;
await ctx . ReadToStreamAsync ( strm ) ;
return strm . ToArray ( ) ;
2022-09-01 05:26:26 +00:00
}
/// <summary>
/// Read request body to array
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <returns>Request body data</returns>
public static byte [ ] ReadBytes ( this ServerContext ctx )
{
MemoryStream strm = new MemoryStream ( ) ;
ctx . ReadToStream ( strm ) ;
return strm . ToArray ( ) ;
2022-04-24 18:56:06 +00:00
}
/// <summary>
/// Read request body to stream
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="strm">Stream to write to</param>
public static async Task ReadToStreamAsync ( this ServerContext ctx , Stream strm )
{
2022-09-01 05:26:26 +00:00
await ctx . GetRequestStream ( ) . CopyToAsync ( strm ) ;
}
/// <summary>
/// Read request body to stream
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="strm">Stream to write to</param>
public static void ReadToStream ( this ServerContext ctx , Stream strm )
{
ctx . GetRequestStream ( ) . CopyTo ( strm ) ;
2022-04-24 18:56:06 +00:00
}
/// <summary>
/// Read request body to file
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="filename">name of file to write too, can be without extension</param>
/// <returns>file path with extension unless mimetype header is missing</returns>
public static async Task < string > ReadToFileAsync ( this ServerContext ctx , string filename )
{
if ( string . IsNullOrWhiteSpace ( Path . GetExtension ( filename ) ) )
{
string val ;
if ( ctx . RequestHeaders . TryGetFirst ( "Content-Type" , out val ) )
{
filename + = $".{MimeTypesMap.GetExtension(val)}" ;
}
}
using ( var f = File . Create ( filename ) )
{
2022-09-01 05:26:26 +00:00
await ctx . ReadToStreamAsync ( f ) ;
2022-04-24 18:56:06 +00:00
}
return filename ;
}
2022-09-01 05:26:26 +00:00
/// <summary>
/// Read request body to file
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="filename">name of file to write too, can be without extension</param>
/// <returns>file path with extension unless mimetype header is missing</returns>
public static string ReadToFile ( this ServerContext ctx , string filename )
{
if ( string . IsNullOrWhiteSpace ( Path . GetExtension ( filename ) ) )
{
string val ;
if ( ctx . RequestHeaders . TryGetFirst ( "Content-Type" , out val ) )
{
filename + = $".{MimeTypesMap.GetExtension(val)}" ;
}
}
using ( var f = File . Create ( filename ) )
{
ctx . ReadToStream ( f ) ;
}
return filename ;
}
2024-03-18 12:31:51 +00:00
2022-04-23 18:25:45 +00:00
/// <summary>
/// Write headers to stream
/// </summary>
/// <param name="ctx">ServerContext</param>
2022-04-02 21:15:20 +00:00
public static async Task WriteHeadersAsync ( this ServerContext ctx )
2022-04-02 18:59:12 +00:00
{
string status_line = $"HTTP/1.1 {ctx.StatusCode} {StatusCodeMap.GetStatusString(ctx.StatusCode)}\r\n" ;
StringBuilder b = new StringBuilder ( status_line ) ;
foreach ( var hdr in ctx . ResponseHeaders )
{
foreach ( var v in hdr . Value )
{
b . Append ( $"{hdr.Key}: {v}\r\n" ) ;
}
}
b . Append ( "\r\n" ) ;
var data = Encoding . UTF8 . GetBytes ( b . ToString ( ) ) ;
await ctx . NetworkStream . WriteAsync ( data , 0 , data . Length ) ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
2022-09-01 05:26:26 +00:00
/// Write headers to stream
/// </summary>
/// <param name="ctx">ServerContext</param>
public static void WriteHeaders ( this ServerContext ctx )
{
string status_line = $"HTTP/1.1 {ctx.StatusCode} {StatusCodeMap.GetStatusString(ctx.StatusCode)}\r\n" ;
StringBuilder b = new StringBuilder ( status_line ) ;
foreach ( var hdr in ctx . ResponseHeaders )
{
foreach ( var v in hdr . Value )
{
b . Append ( $"{hdr.Key}: {v}\r\n" ) ;
}
}
b . Append ( "\r\n" ) ;
var data = Encoding . UTF8 . GetBytes ( b . ToString ( ) ) ;
ctx . NetworkStream . Write ( data , 0 , data . Length ) ;
}
2024-03-18 12:31:51 +00:00
2022-04-23 18:25:45 +00:00
/// <summary>
/// Send exception to client
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="ex">the Exception</param>
2022-04-02 18:59:12 +00:00
public static async Task SendExceptionAsync ( this ServerContext ctx , Exception ex )
{
string name = ex . GetType ( ) . FullName ;
2024-03-18 12:31:51 +00:00
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>" ;
2022-04-02 18:59:12 +00:00
ctx . StatusCode = 500 ;
await ctx . SendTextAsync ( j ) ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
2022-09-01 05:26:26 +00:00
/// Send exception to client
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="ex">the Exception</param>
public static void SendException ( this ServerContext ctx , Exception ex )
{
string name = ex . GetType ( ) . FullName ;
2024-03-18 12:31:51 +00:00
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>" ;
2022-09-01 05:26:26 +00:00
ctx . StatusCode = 500 ;
ctx . SendText ( j ) ;
}
/// <summary>
2022-04-23 18:25:45 +00:00
/// Send object as json to client
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="value">an object to serialize with newtonsoft.json</param>
2022-04-02 18:59:12 +00:00
public static async Task SendJsonAsync ( this ServerContext ctx , object value )
{
await ctx . SendTextAsync ( JsonConvert . SerializeObject ( value ) , "application/json" ) ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
2022-09-01 05:26:26 +00:00
/// Send object as json to client
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="value">an object to serialize with newtonsoft.json</param>
public static void SendJson ( this ServerContext ctx , object value )
{
ctx . SendText ( JsonConvert . SerializeObject ( value ) , "application/json" ) ;
}
/// <summary>
2022-04-23 18:25:45 +00:00
/// Send text to client
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="data">some text</param>
/// <param name="contentType">mime type</param>
2022-04-02 21:15:20 +00:00
public static async Task SendTextAsync ( this ServerContext ctx , string data , string contentType = "text/html" )
2022-04-02 18:59:12 +00:00
{
2022-04-02 21:15:20 +00:00
await ctx . SendBytesAsync ( Encoding . UTF8 . GetBytes ( data ) , contentType ) ;
2022-04-02 18:59:12 +00:00
}
2022-04-23 18:25:45 +00:00
/// <summary>
2022-09-01 05:26:26 +00:00
/// Send text to client
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="data">some text</param>
/// <param name="contentType">mime type</param>
public static void SendText ( this ServerContext ctx , string data , string contentType = "text/html" )
{
ctx . SendBytes ( Encoding . UTF8 . GetBytes ( data ) , contentType ) ;
}
/// <summary>
2022-04-23 18:25:45 +00:00
/// Send redirect
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="url">Url to redirect to</param>
public static async Task SendRedirectAsync ( this ServerContext ctx , string url )
{
ctx . StatusCode = 301 ;
ctx . ResponseHeaders . Add ( "Cache-Control" , "no-cache" ) ;
ctx . ResponseHeaders . Add ( "Location" , url ) ;
await ctx . WriteHeadersAsync ( ) ;
}
/// <summary>
2022-09-01 05:26:26 +00:00
/// Send redirect
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="url">Url to redirect to</param>
public static void SendRedirect ( this ServerContext ctx , string url )
{
ctx . StatusCode = 301 ;
ctx . ResponseHeaders . Add ( "Cache-Control" , "no-cache" ) ;
ctx . ResponseHeaders . Add ( "Location" , url ) ;
ctx . WriteHeaders ( ) ;
}
/// <summary>
2022-04-23 18:25:45 +00:00
/// Send byte[] to client
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="array">a byte[] array</param>
/// <param name="contentType">mime type</param>
2022-04-02 21:15:20 +00:00
public static async Task SendBytesAsync ( this ServerContext ctx , byte [ ] array , string contentType = "application/octet-stream" )
2022-04-02 18:59:12 +00:00
{
using ( var ms = new MemoryStream ( array ) )
{
2022-04-02 21:15:20 +00:00
await ctx . SendStreamAsync ( ms , contentType ) ;
2022-04-02 18:59:12 +00:00
}
}
2022-04-23 18:25:45 +00:00
/// <summary>
2022-09-01 05:26:26 +00:00
/// Send byte[] to client
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <param name="array">a byte[] array</param>
/// <param name="contentType">mime type</param>
public static void SendBytes ( this ServerContext ctx , byte [ ] array , string contentType = "application/octet-stream" )
{
using ( var ms = new MemoryStream ( array ) )
{
ctx . SendStream ( ms , contentType ) ;
}
}
/// <summary>
2022-04-23 18:25:45 +00:00
/// Get first item in Dictionary<T1,List<T2>> based on key
/// </summary>
/// <param name="args">the dictionary with list<T2> value</param>
/// <param name="key">some key</param>
/// <typeparam name="T1">key type</typeparam>
/// <typeparam name="T2">value type</typeparam>
/// <returns></returns>
2022-04-02 18:59:12 +00:00
public static T2 GetFirst < T1 , T2 > ( this Dictionary < T1 , List < T2 > > args , T1 key )
{
return args [ key ] [ 0 ] ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Try to get first item in Dictionary<T1,List<T2>> based on key
/// </summary>
/// <param name="args">the dictionary with list<T2> value</param>
/// <param name="key">the key to check</param>
/// <param name="value">the value returned</param>
/// <typeparam name="T1">key type</typeparam>
/// <typeparam name="T2">value type</typeparam>
/// <returns>true if found else false if not found</returns>
2022-04-03 14:39:54 +00:00
public static bool TryGetFirst < T1 , T2 > ( this Dictionary < T1 , List < T2 > > args , T1 key , out T2 value )
{
List < T2 > ls ;
if ( args . TryGetValue ( key , out ls ) )
{
if ( ls . Count > 0 )
{
value = ls [ 0 ] ;
return true ;
}
}
value = default ( T2 ) ;
return false ;
}
2024-03-31 01:01:24 +00:00
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.0 M ;
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 ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Add item to the Dictionary<T1,List<T2>> with specified key (will create key in dictionary if not exist)
/// </summary>
/// <param name="list">the dictionary with list<T2> value</param>
/// <param name="key">the key to add or to add to</param>
/// <param name="item">a item</param>
/// <typeparam name="T1">key type</typeparam>
/// <typeparam name="T2">value type</typeparam>
2022-04-02 18:59:12 +00:00
public static void Add < T1 , T2 > ( this Dictionary < T1 , List < T2 > > list , T1 key , T2 item )
{
if ( list . ContainsKey ( key ) )
{
list [ key ] . Add ( item ) ;
}
else
{
List < T2 > items = new List < T2 > ( ) ;
items . Add ( item ) ;
list . Add ( key , items ) ;
}
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Add multiple items to the Dictionary<T1,List<T2>> with specified key (will create key in dictionary if not exist)
/// </summary>
/// <param name="list">the dictionary with list<T2> value</param>
/// <param name="key">the key to add or to add to</param>
/// <param name="items">IEnumerable<T2></param>
/// <typeparam name="T1">key type</typeparam>
/// <typeparam name="T2">value type</typeparam>
2022-04-02 18:59:12 +00:00
public static void AddRange < T1 , T2 > ( this Dictionary < T1 , List < T2 > > list , T1 key , IEnumerable < T2 > items )
{
if ( list . ContainsKey ( key ) )
{
list [ key ] . AddRange ( items ) ;
}
else
{
List < T2 > items2 = new List < T2 > ( ) ;
items2 . AddRange ( items ) ;
list . Add ( key , items2 ) ;
}
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// StringBuilder ends with
/// </summary>
/// <param name="sb">string builder</param>
/// <param name="test">text to check</param>
/// <param name="comparison">comparison type</param>
/// <returns>true if sb ends with test, false if it does not</returns>
2022-04-02 18:59:12 +00:00
public static bool EndsWith ( this StringBuilder sb , string test ,
StringComparison comparison )
{
if ( sb . Length < test . Length )
return false ;
string end = sb . ToString ( sb . Length - test . Length , test . Length ) ;
return end . Equals ( test , comparison ) ;
}
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// returns 404 not found page
/// </summary>
2022-12-14 18:08:52 +00:00
public class NotFoundServer : SameServer
2022-04-02 18:59:12 +00:00
{
2022-04-23 18:25:45 +00:00
/// <summary>
/// 404 not found custom html use "{url}" in your html as url
/// </summary>
/// <param name="html">the custom html</param>
2022-12-14 18:08:52 +00:00
public NotFoundServer ( string html ) : base ( html )
2022-04-02 18:59:12 +00:00
{
2022-12-14 18:08:52 +00:00
2022-04-02 18:59:12 +00:00
}
2022-04-23 18:25:45 +00:00
/// <summary>
///404 not found default html
/// </summary>
2022-12-14 18:08:52 +00:00
public NotFoundServer ( ) : base ( "<html><head><title>File {url} not found</title></head><body><h1>404 Not Found</h1><h4>{url}</h4></body></html>" )
2022-04-02 18:59:12 +00:00
{
2022-12-14 18:08:52 +00:00
2022-04-02 18:59:12 +00:00
}
2024-03-18 12:31:51 +00:00
2022-12-14 18:08:52 +00:00
}
public class SameServer : Server
{
/// <summary>
/// html use "{url}" in your html as url
/// </summary>
/// <param name="html">the custom html</param>
/// <param name="statusCode">the status code</param>
public SameServer ( string html , int statusCode = 404 )
{
_html = html ;
_statusCode = statusCode ;
}
int _statusCode ;
2022-04-02 18:59:12 +00:00
string _html ;
public override async Task GetAsync ( ServerContext ctx )
{
2022-12-14 18:08:52 +00:00
ctx . StatusCode = _statusCode ;
2022-04-23 18:25:45 +00:00
await ctx . SendTextAsync ( _html . Replace ( "{url}" , WebUtility . HtmlEncode ( ctx . OriginalUrlPath ) ) ) ;
2022-04-02 18:59:12 +00:00
}
2024-03-18 12:31:51 +00:00
2022-04-02 18:59:12 +00:00
}
2022-12-14 18:08:52 +00:00
public enum WebServerPathType
2022-04-02 18:59:12 +00:00
{
2022-12-14 18:08:52 +00:00
NotFound = - 1 ,
File = 0 ,
Directory = 1
}
public sealed class WebServerPathEntry
{
public WebServerPathEntry ( string fname , WebServerPathType type , string url )
{
FileName = fname ;
Type = type ;
Url = url ;
}
public string FileName { get ; set ; }
public string Url { get ; set ; }
public WebServerPathType Type { get ; set ; }
}
public interface IFileHandler
{
IEnumerable < string > ListDirectory ( string path ) ;
bool DefaultFileExists ( string path , out string name ) ;
WebServerPathEntry GetPath ( string path ) ;
Stream CreateFile ( string path ) ;
Stream Open ( WebServerPathEntry entry ) ;
void CreateDirectory ( string dir ) ;
}
public sealed class WebServerStyleFilesystemHandler : IFileHandler
{
string [ ] _defaultFileNames ;
IVirtualFilesystem _fs ;
2022-04-23 18:25:45 +00:00
/// <summary>
2022-12-14 18:08:52 +00:00
/// construct with filesystem
2022-04-23 18:25:45 +00:00
/// </summary>
2022-12-14 18:08:52 +00:00
/// <param name="fs">filesystem for root</param>
public WebServerStyleFilesystemHandler ( IVirtualFilesystem fs )
2022-04-02 18:59:12 +00:00
{
2022-12-14 18:08:52 +00:00
_defaultFileNames = new string [ ] { "index.html" , "index.htm" , "default.html" , "default.htm" } ;
_fs = fs ;
}
/// <summary>
/// construct with filesystem, custom filenames
/// </summary>
/// <param name="path">filesystem for root</param>
/// <param name="defaultFileNames">like index.html, index.htm, default.html, default.htm</param>
public WebServerStyleFilesystemHandler ( IVirtualFilesystem fs , string [ ] defaultFileNames )
{
_fs = fs ;
_defaultFileNames = defaultFileNames ;
2022-04-02 18:59:12 +00:00
}
2022-12-14 18:08:52 +00:00
public void CreateDirectory ( string dir )
{
_fs . CreateDirectory ( Special . Root / dir ) ;
}
public Stream CreateFile ( string name )
{
return _fs . Open ( Special . Root / name , FileMode . Create , FileAccess . ReadWrite , FileShare . None ) ;
}
public bool DefaultFileExists ( string path , out string name )
{
foreach ( var def in _defaultFileNames )
{
2023-06-08 19:04:59 +00:00
var _name = Special . Root / path / def ; //woops we need the def here
2022-12-14 18:08:52 +00:00
name = _name . Path ;
if ( _fs . FileExists ( _name ) )
{
return true ;
}
}
name = "" ;
return false ;
}
public WebServerPathEntry GetPath ( string path )
{
UnixPath someUrl = WebUtility . UrlDecode ( path ) ;
if ( _fs . DirectoryExists ( someUrl ) )
{
string name ;
if ( DefaultFileExists ( someUrl . Path , out name ) )
{
return new WebServerPathEntry ( name , WebServerPathType . File , path ) ;
} else {
return new WebServerPathEntry ( someUrl . Path , WebServerPathType . Directory , path ) ;
}
}
else if ( _fs . FileExists ( someUrl ) )
{
return new WebServerPathEntry ( someUrl . Path , WebServerPathType . File , path ) ;
}
else
{
return new WebServerPathEntry ( someUrl . Path , WebServerPathType . NotFound , path ) ;
}
}
public IEnumerable < string > ListDirectory ( string path )
{
foreach ( var item in _fs . EnumerateDirectories ( Special . Root / path ) )
{
yield return item . Name + "/" ;
}
foreach ( var item in _fs . EnumerateFiles ( Special . Root / path ) )
{
yield return item . Name ;
}
}
public Stream Open ( WebServerPathEntry path )
{
return _fs . Open ( Special . Root / path . FileName , FileMode . Open , FileAccess . Read , FileShare . Read ) ;
}
}
public sealed class WebServerStyleFileHandler : IFileHandler
{
2022-04-02 18:59:12 +00:00
string [ ] _defaultFileNames ;
2022-12-14 18:08:52 +00:00
string _path ;
2022-04-23 18:25:45 +00:00
/// <summary>
2022-12-14 18:08:52 +00:00
/// construct with path
2022-04-23 18:25:45 +00:00
/// </summary>
2022-12-14 18:08:52 +00:00
/// <param name="path">directory for root</param>
public WebServerStyleFileHandler ( string path )
{
_defaultFileNames = new string [ ] { "index.html" , "index.htm" , "default.html" , "default.htm" } ;
_path = path ;
}
/// <summary>
/// construct with path, custom filenames
/// </summary>
/// <param name="path">directory for root</param>
2022-04-23 18:25:45 +00:00
/// <param name="defaultFileNames">like index.html, index.htm, default.html, default.htm</param>
2022-12-14 18:08:52 +00:00
public WebServerStyleFileHandler ( string path , string [ ] defaultFileNames )
2022-04-02 18:59:12 +00:00
{
_path = path ;
2022-12-14 18:08:52 +00:00
2022-04-02 18:59:12 +00:00
_defaultFileNames = defaultFileNames ;
}
2022-12-14 18:08:52 +00:00
public bool DefaultFileExists ( string path , out string name )
2022-04-02 18:59:12 +00:00
{
foreach ( var def in _defaultFileNames )
{
name = Path . Combine ( path , def ) ;
if ( File . Exists ( name ) )
{
return true ;
}
}
name = "" ;
return false ;
}
2024-03-18 12:31:51 +00:00
//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 ) ;
}
2022-12-14 18:08:52 +00:00
public WebServerPathEntry GetPath ( string url )
2022-04-02 18:59:12 +00:00
{
2024-03-18 12:31:51 +00:00
string someUrl = Path . Combine ( _path , WebUtility . UrlDecode ( FixDotPath ( url ) ) . Replace ( '/' , Path . DirectorySeparatorChar ) ) ;
2022-04-03 15:36:38 +00:00
//Console.WriteLine(someUrl);
2022-04-02 18:59:12 +00:00
if ( Directory . Exists ( someUrl ) )
{
string name ;
if ( DefaultFileExists ( someUrl , out name ) )
{
2022-12-14 18:08:52 +00:00
return new WebServerPathEntry ( name , WebServerPathType . File , url ) ;
} else {
return new WebServerPathEntry ( someUrl , WebServerPathType . Directory , url ) ;
2022-04-02 18:59:12 +00:00
}
}
else if ( File . Exists ( someUrl ) )
{
2022-12-14 18:08:52 +00:00
return new WebServerPathEntry ( someUrl , WebServerPathType . File , url ) ;
2022-04-02 18:59:12 +00:00
}
else
{
2022-12-14 18:08:52 +00:00
return new WebServerPathEntry ( someUrl , WebServerPathType . NotFound , url ) ;
}
}
public IEnumerable < string > ListDirectory ( string path )
{
string someUrl = Path . Combine ( _path , WebUtility . UrlDecode ( path . Substring ( 1 ) ) . Replace ( '/' , Path . DirectorySeparatorChar ) ) ;
foreach ( var item in Directory . GetDirectories ( someUrl ) )
{
yield return $"{Path.GetFileName(item)}/" ;
2022-04-02 18:59:12 +00:00
}
2022-12-14 18:08:52 +00:00
foreach ( var item in Directory . GetFiles ( someUrl ) )
{
yield return Path . GetFileName ( item ) ;
}
}
public Stream CreateFile ( string entry )
{
string someUrl = Path . Combine ( _path , WebUtility . UrlDecode ( entry . Substring ( 1 ) ) . Replace ( '/' , Path . DirectorySeparatorChar ) ) ;
return File . Create ( someUrl ) ;
}
public Stream Open ( WebServerPathEntry entry )
{
return File . OpenRead ( entry . FileName ) ;
}
public void CreateDirectory ( string dir )
{
Directory . CreateDirectory ( Path . Combine ( _path , dir . Substring ( 1 ) . Replace ( '/' , Path . DirectorySeparatorChar ) ) ) ;
2022-04-02 18:59:12 +00:00
}
2022-12-14 18:08:52 +00:00
}
public class StaticServerFileHandler : EventArgs
{
public string RealPath { get ; set ; }
public string CurrentHttpPath { get ; set ; }
public bool HasBeenHandled { get ; set ; } = false ;
2022-04-02 18:59:12 +00:00
2022-12-14 18:08:52 +00:00
public ServerContext Context { get ; set ; }
public bool Cancel { get ; set ; } = false ;
}
public enum UploadAllowedResponse
{
Yes = 0 ,
NoISentResponse = 1 ,
No = 2
2022-04-02 18:59:12 +00:00
}
2022-12-14 18:08:52 +00:00
/// <summary>
/// Serve static files (doesnt allow listing files)
/// </summary>
public class StaticServer : Server
{
public bool AllowUpload { get ; set ; } = false ;
bool allow = false ;
public Func < ServerContext , UploadAllowedResponse > AllowedToUpload { get ; set ; } = DefaultAllowedToUpload ;
public static UploadAllowedResponse DefaultAllowedToUpload ( ServerContext ctx )
{
return UploadAllowedResponse . Yes ;
}
private bool? getEnvVar ( )
{
string allowListStr = Environment . GetEnvironmentVariable ( "TESSES_WEBSERVER_ALLOW_LISTING" ) ;
if ( ! string . IsNullOrWhiteSpace ( allowListStr ) )
{
string allowLst = allowListStr . ToLower ( ) ;
if ( allowLst = = "1" | | allowLst = = "true" | | allowLst = = "on" | | allowLst = = "yes" | | allowLst = = "y" | | allowLst = = "t" | | allowLst = = "allow" | | allowLst = = "allowed" | | allowLst = = "a" )
{
return true ;
}
if ( allowLst = = "0" | | allowLst = = "false" | | allowLst = = "off" | | allowLst = = "no" | | allowLst = = "n" | | allowLst = = "f" | | allowLst = = "deny" | | allowLst = = "denied" | | allowLst = = "d" )
{
return false ;
}
}
return null ;
}
public bool AllowListingDirectories { get {
bool? var = getEnvVar ( ) ;
if ( ! var . HasValue )
{
return allow ;
}
return var . Value ;
} set {
allow = value ;
} }
public EventHandler < StaticServerFileHandler > FileHandler ;
IServer _server ;
IFileHandler fileHandler ;
IServer _forbidden ;
/// <summary>
/// construct with filesystem
/// </summary>
/// <param name="path">filesystem for server</param>
public StaticServer ( IVirtualFilesystem fs )
{
_server = new NotFoundServer ( ) ;
fileHandler = new WebServerStyleFilesystemHandler ( fs ) ;
_forbidden = new SameServer ( "<html><head><title>File {url} not accessable</title></head><body><h1>403 Forbidden</h1><h4>{url}</h4></body></html>" , 403 ) ;
}
/// <summary>
/// construct with filesystem, custom filenames, and server for not found
/// </summary>
/// <param name="path">filesystem for server</param>
/// <param name="defaultFileNames">like index.html, index.htm, default.html, default.htm</param>
/// <param name="notfoundserver">404 not found server</param>
public StaticServer ( IVirtualFilesystem fs , string [ ] defaultFileNames , IServer notfoundserver )
{
_server = notfoundserver ;
fileHandler = new WebServerStyleFilesystemHandler ( fs , defaultFileNames ) ;
_forbidden = new SameServer ( "<html><head><title>File {url} not accessable</title></head><body><h1>403 Forbidden</h1><h4>{url}</h4></body></html>" , 403 ) ;
}
/// <summary>
/// construct with filesystem, custom filenames, and server for not found
/// </summary>
/// <param name="path">filesystem for server</param>
/// <param name="defaultFileNames">like index.html, index.htm, default.html, default.htm</param>
/// <param name="notfoundserver">404 not found server</param>
/// <param name="allowListing">whether to allow listing directory or not (overridable by environment variable with TESSES_WEBSERVER_ALLOW_LISTING=true|false)
public StaticServer ( IVirtualFilesystem fs , string [ ] defaultFileNames , IServer notfoundserver , bool allowListing )
{
_server = notfoundserver ;
fileHandler = new WebServerStyleFilesystemHandler ( fs , defaultFileNames ) ;
AllowListingDirectories = allowListing ;
_forbidden = new SameServer ( "<html><head><title>File {url} not accessable</title></head><body><h1>403 Forbidden</h1><h4>{url}</h4></body></html>" , 403 ) ;
}
/// <summary>
/// construct with filesystem, custom filenames, and server for not found, server for forbidden and with option to allow/deny listing directories
/// </summary>
/// <param name="path">filesystem for server</param>
/// <param name="defaultFileNames">like index.html, index.htm, default.html, default.htm</param>
/// <param name="notfoundserver">404 not found server</param>
/// <param name="forbidden">server for forbidden files/denied listing</param>
/// <param name="allowListing">whether to allow listing directory or not (overridable by environment variable with TESSES_WEBSERVER_ALLOW_LISTING=true|false)
public StaticServer ( IVirtualFilesystem fs , string [ ] defaultFileNames , IServer notfoundserver , IServer forbidden , bool allowListing )
{
_server = notfoundserver ;
fileHandler = fileHandler = new WebServerStyleFilesystemHandler ( fs , defaultFileNames ) ;
AllowListingDirectories = allowListing ;
_forbidden = forbidden ;
}
/// <summary>
/// construct with filesystem, custom filenames and with option to allow/deny listing directories
/// </summary>
/// <param name="path">filesystem for server</param>
/// <param name="defaultFileNames">like index.html, index.htm, default.html, default.htm</param>
/// <param name="allowListing">whether to allow listing directory or not (overridable by environment variable with TESSES_WEBSERVER_ALLOW_LISTING=true|false)
public StaticServer ( IVirtualFilesystem fs , string [ ] defaultFileNames , bool allowListing )
{
_server = new NotFoundServer ( ) ;
fileHandler = new WebServerStyleFilesystemHandler ( fs , defaultFileNames ) ;
AllowListingDirectories = allowListing ;
_forbidden = new SameServer ( "<html><head><title>File {url} not accessable</title></head><body><h1>403 Forbidden</h1><h4>{url}</h4></body></html>" , 403 ) ;
}
/// <summary>
/// construct with filesystem and with option to allow/deny listing directories
/// </summary>
/// <param name="path">filesystem for server</param>
/// <param name="allowListing">whether to allow listing directory or not (overridable by environment variable with TESSES_WEBSERVER_ALLOW_LISTING=true|false)
public StaticServer ( IVirtualFilesystem fs , bool allowListing )
{
_server = new NotFoundServer ( ) ;
fileHandler = new WebServerStyleFilesystemHandler ( fs ) ;
AllowListingDirectories = allowListing ;
_forbidden = new SameServer ( "<html><head><title>File {url} not accessable</title></head><body><h1>403 Forbidden</h1><h4>{url}</h4></body></html>" , 403 ) ;
}
/// <summary>
/// construct with path
/// </summary>
/// <param name="path">directory for server</param>
public StaticServer ( string path )
{
_server = new NotFoundServer ( ) ;
fileHandler = new WebServerStyleFileHandler ( path ) ;
_forbidden = new SameServer ( "<html><head><title>File {url} not accessable</title></head><body><h1>403 Forbidden</h1><h4>{url}</h4></body></html>" , 403 ) ;
}
/// <summary>
/// construct with path, custom filenames, and server for not found
/// </summary>
/// <param name="path">directory for server</param>
/// <param name="defaultFileNames">like index.html, index.htm, default.html, default.htm</param>
/// <param name="notfoundserver">404 not found server</param>
public StaticServer ( string path , string [ ] defaultFileNames , IServer notfoundserver )
{
_server = notfoundserver ;
fileHandler = new WebServerStyleFileHandler ( path , defaultFileNames ) ;
_forbidden = new SameServer ( "<html><head><title>File {url} not accessable</title></head><body><h1>403 Forbidden</h1><h4>{url}</h4></body></html>" , 403 ) ;
}
/// <summary>
/// construct with path, custom filenames, and server for not found
/// </summary>
/// <param name="path">directory for server</param>
/// <param name="defaultFileNames">like index.html, index.htm, default.html, default.htm</param>
/// <param name="notfoundserver">404 not found server</param>
/// <param name="allowListing">whether to allow listing directory or not (overridable by environment variable with TESSES_WEBSERVER_ALLOW_LISTING=true|false)
public StaticServer ( string path , string [ ] defaultFileNames , IServer notfoundserver , bool allowListing )
{
_server = notfoundserver ;
fileHandler = new WebServerStyleFileHandler ( path , defaultFileNames ) ;
AllowListingDirectories = allowListing ;
_forbidden = new SameServer ( "<html><head><title>File {url} not accessable</title></head><body><h1>403 Forbidden</h1><h4>{url}</h4></body></html>" , 403 ) ;
}
/// <summary>
/// construct with path, custom filenames, and server for not found, server for forbidden and with option to allow/deny listing directories
/// </summary>
/// <param name="path">directory for server</param>
/// <param name="defaultFileNames">like index.html, index.htm, default.html, default.htm</param>
/// <param name="notfoundserver">404 not found server</param>
/// <param name="forbidden">server for forbidden files/denied listing</param>
/// <param name="allowListing">whether to allow listing directory or not (overridable by environment variable with TESSES_WEBSERVER_ALLOW_LISTING=true|false)
public StaticServer ( string path , string [ ] defaultFileNames , IServer notfoundserver , IServer forbidden , bool allowListing )
{
_server = notfoundserver ;
fileHandler = new WebServerStyleFileHandler ( path , defaultFileNames ) ;
AllowListingDirectories = allowListing ;
_forbidden = forbidden ;
}
/// <summary>
/// construct with path, custom filenames and with option to allow/deny listing directories
/// </summary>
/// <param name="path">directory for server</param>
/// <param name="defaultFileNames">like index.html, index.htm, default.html, default.htm</param>
/// <param name="allowListing">whether to allow listing directory or not (overridable by environment variable with TESSES_WEBSERVER_ALLOW_LISTING=true|false)
public StaticServer ( string path , string [ ] defaultFileNames , bool allowListing )
{
_server = new NotFoundServer ( ) ;
fileHandler = new WebServerStyleFileHandler ( path , defaultFileNames ) ;
AllowListingDirectories = allowListing ;
_forbidden = new SameServer ( "<html><head><title>File {url} not accessable</title></head><body><h1>403 Forbidden</h1><h4>{url}</h4></body></html>" , 403 ) ;
}
2024-03-18 12:31:51 +00:00
public bool RedirectToRootInsteadOfNotFound { get ; set ; } = false ;
2022-12-14 18:08:52 +00:00
/// <summary>
/// construct with path and with option to allow/deny listing directories
/// </summary>
/// <param name="path">directory for server</param>
/// <param name="allowListing">whether to allow listing directory or not (overridable by environment variable with TESSES_WEBSERVER_ALLOW_LISTING=true|false)
2024-03-18 12:31:51 +00:00
2022-12-14 18:08:52 +00:00
public StaticServer ( string path , bool allowListing )
{
_server = new NotFoundServer ( ) ;
fileHandler = new WebServerStyleFileHandler ( path ) ;
AllowListingDirectories = allowListing ;
_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 DefaultFileExists ( string path , out string name )
{
return fileHandler . DefaultFileExists ( path , out name ) ;
}
public override async Task OtherAsync ( ServerContext ctx )
{
if ( ctx . Method = = "PUT" )
{
if ( AllowUpload )
{
var res = AllowedToUpload ( ctx ) ;
if ( res = = UploadAllowedResponse . Yes )
{
bool fileExists = fileHandler . GetPath ( ctx . UrlPath ) . Type = = WebServerPathType . File ;
using ( var file = fileHandler . CreateFile ( ctx . UrlPath ) )
{
await ctx . ReadToStreamAsync ( file ) ;
}
ctx . StatusCode = fileExists ? 204 : 201 ;
ctx . ResponseHeaders . Add ( "Content-Location" , ctx . UrlPath ) ;
await ctx . WriteHeadersAsync ( ) ;
} else if ( res = = UploadAllowedResponse . No )
{
await _forbidden . GetAsync ( ctx ) ;
}
}
}
}
public override async Task PostAsync ( ServerContext ctx )
{
var fileEntry = fileHandler . GetPath ( ctx . UrlPath ) ;
if ( fileEntry . Type = = WebServerPathType . Directory )
{
if ( AllowUpload )
{
var res = AllowedToUpload ( ctx ) ;
if ( res = = UploadAllowedResponse . Yes )
{
//upload files and dirs
var res0 = ctx . ParseBody ( ( fieldName , fileName , ctype ) = > {
return fileHandler . CreateFile ( $"{ctx.UrlPath}/{fileName.TrimStart('/')}" ) ;
} ) ;
foreach ( var item in res0 )
{
item . Value . Dispose ( ) ;
}
List < string > dirs = new List < string > ( ) ;
if ( ctx . QueryParams . TryGetValue ( "mkdir" , out dirs ) )
{
foreach ( var dir in dirs )
{
fileHandler . CreateDirectory ( $"{ctx.UrlPath}/{dir.TrimStart('/')}" ) ;
}
}
} else if ( res = = UploadAllowedResponse . No )
{
await _forbidden . GetAsync ( ctx ) ;
}
}
}
}
public override async Task GetAsync ( ServerContext ctx )
{
var fileEntry = fileHandler . GetPath ( ctx . UrlPath ) ;
try {
switch ( fileEntry . Type )
{
case WebServerPathType . File :
using ( var strm = fileHandler . Open ( fileEntry ) )
await ctx . SendStreamAsync ( strm , HeyRed . Mime . MimeTypesMap . GetMimeType ( fileEntry . FileName ) ) ;
break ;
case WebServerPathType . Directory :
if ( AllowListingDirectories )
{
DirectoryLister lister = new DirectoryLister ( ) ;
lister . FromDirectory ( fileHandler , ctx . UrlPath , ctx . OriginalUrlPath ) ;
await lister . GetAsync ( ctx ) ;
}
else
{
await _forbidden . GetAsync ( ctx ) ;
}
break ;
case WebServerPathType . NotFound :
2023-02-12 05:55:59 +00:00
if ( RedirectToRootInsteadOfNotFound )
{
2023-06-08 19:04:59 +00:00
2023-02-12 05:55:59 +00:00
var fileEntry2 = fileHandler . GetPath ( "/" ) ;
2023-06-08 19:04:59 +00:00
switch ( fileEntry2 . Type )
2023-02-12 05:55:59 +00:00
{
case WebServerPathType . File :
using ( var strm = fileHandler . Open ( fileEntry2 ) )
2023-06-08 19:04:59 +00:00
await ctx . SendStreamAsync ( strm , HeyRed . Mime . MimeTypesMap . GetMimeType ( fileEntry2 . FileName ) ) ;
2023-02-12 05:55:59 +00:00
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 ) ;
}
2022-12-14 18:08:52 +00:00
break ;
}
} catch ( UnauthorizedAccessException ex )
{
_ = ex ;
await _forbidden . GetAsync ( ctx ) ;
}
}
}
public class DirectoryLister : Server
{
public static string GetFileName ( string url )
{
if ( Path . DirectorySeparatorChar = = '/' )
{
return Path . GetFileName ( url ) ;
}
return Path . GetFileName ( url . Replace ( '/' , Path . DirectorySeparatorChar ) ) ;
}
private static string _getFileNameWithoutQuery ( string url )
{
int r = url . IndexOf ( '?' ) ;
if ( r > - 1 )
{
return GetFileName ( url . Remove ( r ) . TrimEnd ( '/' ) ) + ( url . EndsWith ( "/" ) ? "/" : "" ) ;
}
return GetFileName ( url . TrimEnd ( '/' ) ) + ( url . EndsWith ( "/" ) ? "/" : "" ) ;
}
static string defaultText = GetEnumerable ( "Default" , "" , new string [ 0 ] ) ;
string curText = defaultText ;
public static string GetEnumerable ( string title , string desc , IEnumerable < string > entries )
{
/ * if ( ! this . ln . Path . EndsWith ( "/" ) )
{
SendRedirect ( this . ln . Path + '/' ) ;
return ;
} * /
StringBuilder b = new StringBuilder ( ) ;
b . Append ( $"<!doctype><html><head><title>{HttpUtility.HtmlEncode(title)}</title></head><body><center><h1>{HttpUtility.HtmlEncode(title)}</h1></center><center>{HttpUtility.HtmlEncode(desc)}</center><hr><a href=\" . . \ ">../</a><br>" ) ;
foreach ( var entry in entries )
{
//<a href=
b . Append ( $"<a href=\" { HttpUtility . HtmlAttributeEncode ( entry ) } \ ">{HttpUtility.HtmlEncode(_getFileNameWithoutQuery(entry))}</a><br>" ) ;
}
b . Append ( "<hr><br><center><a href=\"https://www.nuget.org/packages/Tesses.WebServer\">Tesses.WebServer</a></center></body></html>" ) ;
return b . ToString ( ) ;
}
public string Title { get ; set ; } = "Directory Listing" ;
public string Description { get ; set ; } = "" ;
public void FromEnumerable ( string title , string desc , IEnumerable < string > entries )
{
curText = GetEnumerable ( title , desc , entries ) ;
}
public void FromDirectory ( string title , string desc , string directory , string reqPath )
{
List < string > items = new List < string > ( ) ;
foreach ( var item in Directory . EnumerateDirectories ( directory ) )
{
items . Add ( item + "/" ) ;
}
foreach ( var item in Directory . EnumerateFiles ( directory ) )
{
items . Add ( item ) ;
}
FromEnumerable ( title , desc , items ) ;
}
public void FromDirectory ( string directory , string reqPath )
{
FromDirectory ( $"Index of {reqPath}" , "Directory listing" , directory , reqPath ) ;
}
public void FromDirectory ( IFileHandler handler , string reqPath , string ogPath = "/" )
{
FromEnumerable ( $"Index of {ogPath}" , "Directory listing" , handler . ListDirectory ( reqPath ) ) ;
}
public override async Task GetAsync ( ServerContext ctx )
{
await ctx . SendTextAsync ( curText ) ;
}
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Server where you can change inner server
/// </summary>
public class ChangeableServer : Server
{
/// <summary>
/// The inner server to change
/// </summary>
public IServer Server { get ; set ; }
/// <summary>
/// Construct with default value
/// </summary>
public ChangeableServer ( )
{
Server = null ;
}
/// <summary>
/// Construct with server
/// </summary>
/// <param name="svr">the inner server</param>
public ChangeableServer ( IServer svr )
{
Server = svr ;
}
public override async Task < bool > BeforeAsync ( ServerContext ctx )
{
return await Guaranteed ( Server ) . BeforeAsync ( ctx ) ;
}
public override async Task GetAsync ( ServerContext ctx )
{
await Guaranteed ( Server ) . GetAsync ( ctx ) ;
}
public override async Task OptionsAsync ( ServerContext ctx )
{
await Guaranteed ( Server ) . OptionsAsync ( ctx ) ;
}
public override async Task OtherAsync ( ServerContext ctx )
{
await Guaranteed ( Server ) . OtherAsync ( ctx ) ;
}
public override async Task PostAsync ( ServerContext ctx )
{
await Guaranteed ( Server ) . PostAsync ( ctx ) ;
}
}
/// <summary>
2024-03-18 12:31:51 +00:00
/// 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>
2022-04-23 18:25:45 +00:00
/// Abstract class for server
/// </summary>
2022-04-02 18:59:12 +00:00
public abstract class Server : IServer
{
2022-04-23 18:25:45 +00:00
/// <summary>
/// Returns 404 Not found
/// </summary>
2022-04-03 14:39:54 +00:00
public static readonly NotFoundServer ServerNull = new NotFoundServer ( ) ;
2022-04-23 18:25:45 +00:00
/// <summary>
/// You are guarenteed to have a server
/// </summary>
/// <param name="svr">any server object</param>
/// <returns>if null return ServerNull otherwise return svr</returns>
2022-04-03 14:39:54 +00:00
public IServer Guaranteed ( IServer svr )
{
if ( svr ! = null )
{
return svr ;
}
return ServerNull ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Put cors header
/// </summary>
2022-04-02 21:15:20 +00:00
public bool CorsHeader = true ;
2022-04-23 18:25:45 +00:00
/// <summary>
/// Called on GET Request
/// </summary>
/// <param name="ctx">ServerContext</param>
2022-04-02 18:59:12 +00:00
public abstract Task GetAsync ( ServerContext ctx ) ;
2022-04-23 18:25:45 +00:00
/// <summary>
/// Called on POST Request
/// </summary>
/// <param name="ctx">ServerContext</param>
2022-04-02 18:59:12 +00:00
public virtual async Task PostAsync ( ServerContext ctx )
{
ctx . StatusCode = ( int ) HttpStatusCode . MethodNotAllowed ;
await ctx . SendTextAsync ( "Method Not Supported" ) ;
2022-09-01 05:26:26 +00:00
2022-04-02 21:15:20 +00:00
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Called on OPTIONS Request
/// </summary>
/// <param name="ctx">ServerContext</param>
2022-04-02 21:15:20 +00:00
public virtual async Task OptionsAsync ( ServerContext ctx )
{
await ctx . WriteHeadersAsync ( ) ;
ctx . NetworkStream . Close ( ) ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Called on any other Request method
/// </summary>
/// <param name="ctx">ServerContext</param>
2022-04-02 21:15:20 +00:00
public virtual async Task OtherAsync ( ServerContext ctx )
{
ctx . StatusCode = ( int ) HttpStatusCode . MethodNotAllowed ;
await ctx . SendTextAsync ( "Method Not Supported" ) ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Called before request was made
/// </summary>
/// <param name="ctx">ServerContext</param>
/// <returns>true to cancel request, false to continue request</returns>
2022-04-02 21:15:20 +00:00
public virtual async Task < bool > BeforeAsync ( ServerContext ctx )
{
return await Task . FromResult ( false ) ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Add cors header
/// </summary>
/// <param name="ctx">Server Context</param>
2022-04-03 14:39:54 +00:00
public void AddCors ( ServerContext ctx )
{
if ( CorsHeader )
{
ctx . ResponseHeaders . Add ( "Access-Control-Allow-Origin" , "*" ) ;
ctx . ResponseHeaders . Add ( "Access-Control-Allow-Headers" , "Cache-Control, Pragma, Accept, Origin, Authorization, Content-Type, X-Requested-With" ) ;
ctx . ResponseHeaders . Add ( "Access-Control-Allow-Methods" , "GET, POST" ) ;
ctx . ResponseHeaders . Add ( "Access-Control-Allow-Credentials" , "true" ) ;
}
}
2024-03-18 12:31:51 +00:00
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 ) ;
}
}
2022-04-02 21:15:20 +00:00
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// mount multiple servers at different url paths
/// </summary>
2022-04-02 21:15:20 +00:00
public sealed class MountableServer : Server
{
Dictionary < string , IServer > _servers = new Dictionary < string , IServer > ( ) ;
public MountableServer ( IServer root )
{
_root = root ;
2023-06-08 19:04:59 +00:00
}
public MountableServer ( ) : this ( new NotFoundServer ( ) )
{
2022-04-02 21:15:20 +00:00
}
IServer _root ;
2022-04-03 14:39:54 +00:00
private ( string Key , IServer Value ) GetFromPath ( ServerContext ctx )
2022-04-02 21:15:20 +00:00
{
//bool j = false;
foreach ( var item in _servers . Reverse ( ) )
{
if ( ctx . UrlPath . StartsWith ( item . Key , StringComparison . Ordinal ) )
{
2022-04-03 14:39:54 +00:00
return ( item . Key , Guaranteed ( item . Value ) ) ;
}
if ( ctx . UrlPath = = item . Key . TrimEnd ( '/' ) )
{
ctx . UrlPath + = "/" ;
return ( item . Key , Guaranteed ( item . Value ) ) ;
2022-04-02 21:15:20 +00:00
}
}
2022-04-03 14:39:54 +00:00
//Console.WriteLine("HERE WE ARE");
return ( "/" , Guaranteed ( _root ) ) ;
2022-04-02 21:15:20 +00:00
}
/// <summary>
/// Mount the specified url and server.
/// Must mount like this
/// /somePath0
/// /somePath0/someSubPath0
/// /somePath0/someSubPath0/someSubSubPath0
/// /somePath0/someSubPath0/someSubSubPath1
/// /somePath0/someSubPath1
/// /somePath0/someSubPath1/someSubSubPath0
/// /somePath0/someSubPath1/someSubSubPath1
/// </summary>
/// <param name="url">URL.</param>
/// <param name="server">Server.</param>
public void Mount ( string url , IServer server )
{
_servers . Add ( url , server ) ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Unmount a server
/// </summary>
/// <param name="url">Url</param>
2022-04-02 21:15:20 +00:00
public void Unmount ( string url )
{
_servers . Remove ( url ) ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Unmount all servers
/// </summary>
2022-04-02 21:15:20 +00:00
public void UnmountAll ( )
{
_servers . Clear ( ) ;
}
public override async Task GetAsync ( ServerContext ctx )
{
var v = GetFromPath ( ctx ) ;
string url = '/' + ctx . UrlPath . Substring ( v . Key . Length ) . TrimStart ( '/' ) ;
ctx . UrlPath = url ;
await v . Value . GetAsync ( ctx ) ;
}
public override async Task PostAsync ( ServerContext ctx )
{
var v = GetFromPath ( ctx ) ;
string url = '/' + ctx . UrlPath . Substring ( v . Key . Length ) . TrimStart ( '/' ) ;
ctx . UrlPath = url ;
await v . Value . PostAsync ( ctx ) ;
}
public override async Task < bool > BeforeAsync ( ServerContext ctx )
{
var v = GetFromPath ( ctx ) ;
string old = ctx . UrlPath ;
string url = '/' + ctx . UrlPath . Substring ( v . Key . Length ) . TrimStart ( '/' ) ;
ctx . UrlPath = url ;
var res = await v . Value . BeforeAsync ( ctx ) ;
ctx . UrlPath = old ;
return res ;
}
public override async Task OptionsAsync ( ServerContext ctx )
{
var v = GetFromPath ( ctx ) ;
string url = '/' + ctx . UrlPath . Substring ( v . Key . Length ) . TrimStart ( '/' ) ;
ctx . UrlPath = url ;
await v . Value . OptionsAsync ( ctx ) ;
}
public override async Task OtherAsync ( ServerContext ctx )
{
var v = GetFromPath ( ctx ) ;
string url = '/' + ctx . UrlPath . Substring ( v . Key . Length ) . TrimStart ( '/' ) ;
ctx . UrlPath = url ;
await v . Value . OtherAsync ( ctx ) ;
2022-04-02 18:59:12 +00:00
}
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Check username and password are correct or if request can be anonymous
/// </summary>
/// <param name="username">Username, can and will be "" on first request for resource</param>
/// <param name="password">Password, can and will be "" on first request for resource</param>
/// <returns>true for authorized, false for unauthorized</returns>
2022-04-03 14:39:54 +00:00
public delegate bool Authenticate ( string username , string password ) ;
2022-04-23 18:25:45 +00:00
/// <summary>
/// Check username and password are correct or if request can be anonymous
/// </summary>
/// <param name="context">Server Context</param>
/// <param name="username">Username, can and will be "" on first request for resource</param>
/// <param name="password">Password, can and will be "" on first request for resource</param>
/// <returns>true for authorized, false for unauthorized</returns>
public delegate bool AuthenticateWithContext ( ServerContext context , string username , string password ) ;
/// <summary>
/// Protect server with password
/// </summary>
2022-04-03 14:39:54 +00:00
public class BasicAuthServer : Server
{
2022-04-23 18:25:45 +00:00
/// <summary>
/// Construct server for user authorization
/// </summary>
/// <param name="auth">callback for authorization</param>
/// <param name="inner">server to protect</param>
/// <param name="realm">realm parameter in WWW-Auhenticate Header</param>
2022-04-03 14:39:54 +00:00
public BasicAuthServer ( Authenticate auth , IServer inner , string realm = "SampleRealm" )
{
Authenticate = auth ;
InnerServer = inner ;
Realm = realm ;
2022-04-23 18:25:45 +00:00
}
/// <summary>
/// Construct server for user authorization (With ServerContext in callback)
/// </summary>
/// <param name="auth">callback for authorization</param>
/// <param name="inner">server to protect</param>
/// <param name="realm">realm parameter in WWW-Auhenticate Header</param>
public BasicAuthServer ( AuthenticateWithContext auth , IServer inner , string realm = "SampleRealm" )
{
AuthenticateWithContext = auth ;
InnerServer = inner ;
Realm = realm ;
2022-04-03 14:39:54 +00:00
}
public override async Task < bool > BeforeAsync ( ServerContext ctx )
{
if ( await Authorize ( ctx ) )
{
return await Guaranteed ( InnerServer ) . BeforeAsync ( ctx ) ;
}
return true ;
}
public override async Task GetAsync ( ServerContext ctx )
{
await Guaranteed ( InnerServer ) . GetAsync ( ctx ) ;
}
public override async Task PostAsync ( ServerContext ctx )
{
await Guaranteed ( InnerServer ) . PostAsync ( ctx ) ;
}
public override async Task OtherAsync ( ServerContext ctx )
{
await Guaranteed ( InnerServer ) . OtherAsync ( ctx ) ;
}
public override async Task OptionsAsync ( ServerContext ctx )
{
await Guaranteed ( InnerServer ) . OptionsAsync ( ctx ) ;
}
2022-04-23 18:25:45 +00:00
/// <summary>
/// Server to protect
/// </summary>
2022-04-03 14:39:54 +00:00
public IServer InnerServer { get ; set ; }
2022-04-23 18:25:45 +00:00
/// <summary>
/// Authentication callback without ServerContext
/// </summary>
2022-04-03 14:39:54 +00:00
public Authenticate Authenticate { get ; set ; }
2022-04-23 18:25:45 +00:00
/// <summary>
/// Authentication callback with ServerContext
/// </summary>
public AuthenticateWithContext AuthenticateWithContext { get ; set ; }
/// <summary>
/// Realm parameter in WWW-Authenticate header
/// </summary>
2022-04-03 14:39:54 +00:00
public string Realm { get ; set ; }
private bool ValidAuth ( ServerContext ctx )
{
string auth ;
2022-04-23 18:25:45 +00:00
if ( Authenticate = = null & & AuthenticateWithContext = = null ) return true ;
2022-04-03 14:39:54 +00:00
if ( ctx . RequestHeaders . TryGetFirst ( "Authorization" , out auth ) )
{
string [ ] authorization = auth . Split ( ' ' ) ;
//authorization_basic
if ( authorization [ 0 ] = = "Basic" )
{
string [ ] userPass = Encoding . UTF8 . GetString ( Convert . FromBase64String ( authorization [ 1 ] ) ) . Split ( new char [ ] { ':' } , 2 ) ;
//return userPass.Equals($"{config.UserName}:{config.Password}", StringComparison.Ordinal);
2022-04-23 18:25:45 +00:00
if ( Authenticate ! = null )
return Authenticate ( userPass [ 0 ] , userPass [ 1 ] ) ;
if ( AuthenticateWithContext ! = null )
return AuthenticateWithContext ( ctx , userPass [ 0 ] , userPass [ 2 ] ) ;
2022-04-03 14:39:54 +00:00
}
2022-04-23 18:25:45 +00:00
} else {
if ( Authenticate ! = null )
return Authenticate ( "" , "" ) ;
if ( AuthenticateWithContext ! = null )
return AuthenticateWithContext ( ctx , "" , "" ) ;
2022-04-03 14:39:54 +00:00
}
return false ;
}
private async Task < bool > Authorize ( ServerContext ctx )
{
2022-04-23 18:25:45 +00:00
if ( Authenticate = = null & & AuthenticateWithContext = = null )
2022-04-03 14:39:54 +00:00
return true ;
if ( ValidAuth ( ctx ) )
return true ;
ctx . ResponseHeaders . Add ( "WWW-Authenticate" , $"Basic realm=\" { Realm } \ "" ) ;
ctx . StatusCode = 401 ;
await UnauthorizedPage ( ctx ) ;
return false ;
}
protected virtual async Task UnauthorizedPage ( ServerContext ctx )
{
await ctx . SendTextAsync ( "Unauthorized" ) ;
}
}
public class HostDomainServer : Server
{
public HostDomainServer ( IServer alt )
{
Default = alt ;
Servers = new Dictionary < string , IServer > ( ) ;
}
public void Clear ( )
{
Servers . Clear ( ) ;
}
public void Remove ( string fqdn_or_ip )
{
Servers . Remove ( fqdn_or_ip ) ;
}
public IServer Default { get ; set ; }
Dictionary < string , IServer > Servers ;
2022-04-02 18:59:12 +00:00
2022-04-03 14:39:54 +00:00
public void AddDomain ( string fqdn_or_ip , IServer svr )
{
Servers . Add ( fqdn_or_ip , svr ) ;
}
public override async Task < bool > BeforeAsync ( ServerContext ctx )
{
return await GetDomain ( ctx ) . BeforeAsync ( ctx ) ;
}
public override async Task PostAsync ( ServerContext ctx )
{
await GetDomain ( ctx ) . PostAsync ( ctx ) ;
}
public override async Task OtherAsync ( ServerContext ctx )
{
await GetDomain ( ctx ) . OtherAsync ( ctx ) ;
}
public override async Task OptionsAsync ( ServerContext ctx )
{
await GetDomain ( ctx ) . OptionsAsync ( ctx ) ;
}
public override async Task GetAsync ( ServerContext ctx )
{
await GetDomain ( ctx ) . GetAsync ( ctx ) ;
}
private IServer GetDomain ( ServerContext ctx )
{
string fqdn_or_ip = ctx . Host ;
foreach ( var item in Servers )
{
if ( item . Key . Equals ( fqdn_or_ip , StringComparison . Ordinal ) )
{
return Guaranteed ( item . Value ) ;
}
}
return Guaranteed ( Default ) ;
}
}
2022-04-02 18:59:12 +00:00
public interface IServer
{
2022-04-03 14:39:54 +00:00
void AddCors ( ServerContext ctx ) ;
2022-04-02 21:15:20 +00:00
Task < bool > BeforeAsync ( ServerContext ctx ) ;
2022-04-02 18:59:12 +00:00
Task GetAsync ( ServerContext ctx ) ;
Task PostAsync ( ServerContext ctx ) ;
2022-04-02 21:15:20 +00:00
Task OptionsAsync ( ServerContext ctx ) ;
Task OtherAsync ( ServerContext ctx ) ;
2022-04-02 18:59:12 +00:00
}
2024-03-18 12:31:51 +00:00
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 ) ;
2024-03-31 01:01:24 +00:00
2024-03-18 12:31:51 +00:00
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 ) ;
}
}
}
}
2022-04-02 18:59:12 +00:00
public sealed class HttpServerListener
{
2022-04-23 18:25:45 +00:00
/// <summary>
/// Print urls when running
/// </summary>
/// <value>true if verbose, false if not</value>
2022-04-03 15:36:38 +00:00
public bool PrintUrls { get ; set ; }
2022-04-03 14:39:54 +00:00
bool https ;
X509Certificate cert ;
2022-09-01 05:26:26 +00:00
ChangeableServer _server ;
2022-04-02 18:59:12 +00:00
TcpListener _listener ;
2022-04-03 15:36:38 +00:00
SslProtocols protocols ;
2022-09-01 05:26:26 +00:00
public HttpServerListener ( int port )
{
_server = new ChangeableServer ( ) ;
_listener = new TcpListener ( new IPEndPoint ( IPAddress . Any , port ) ) ;
https = false ;
PrintUrls = false ;
}
public HttpServerListener ( IPEndPoint endpoint )
{
_server = new ChangeableServer ( ) ;
_listener = new TcpListener ( endpoint ) ;
https = false ;
PrintUrls = false ;
2022-04-24 18:56:06 +00:00
2022-09-01 05:26:26 +00:00
}
public HttpServerListener ( int port , IServer server )
{
_server = new ChangeableServer ( ) ;
_listener = new TcpListener ( new IPEndPoint ( IPAddress . Any , port ) ) ;
_server . Server = server ;
https = false ;
PrintUrls = false ;
}
2022-04-02 18:59:12 +00:00
public HttpServerListener ( IPEndPoint endPoint , IServer server )
{
2022-09-01 05:26:26 +00:00
_server = new ChangeableServer ( ) ;
2022-04-02 18:59:12 +00:00
_listener = new TcpListener ( endPoint ) ;
2022-09-01 05:26:26 +00:00
_server . Server = server ;
2022-04-03 14:39:54 +00:00
https = false ;
2022-04-03 15:36:38 +00:00
PrintUrls = false ;
2022-04-03 14:39:54 +00:00
}
public HttpServerListener ( IServer server )
{
2022-09-01 05:26:26 +00:00
_server = new ChangeableServer ( ) ;
_listener = new TcpListener ( new IPEndPoint ( IPAddress . Any , 3251 ) ) ;
_server . Server = server ;
https = false ;
PrintUrls = false ;
}
public HttpServerListener ( )
{
_server = new ChangeableServer ( ) ;
2022-04-03 14:39:54 +00:00
_listener = new TcpListener ( new IPEndPoint ( IPAddress . Any , 3251 ) ) ;
https = false ;
2022-04-03 15:36:38 +00:00
PrintUrls = false ;
2022-04-03 14:39:54 +00:00
}
2022-09-01 05:26:26 +00:00
public HttpServerListener ( int port , IServer server , X509Certificate cert , SslProtocols protocols = SslProtocols . Default )
{
_server = new ChangeableServer ( ) ;
_listener = new TcpListener ( new IPEndPoint ( IPAddress . Any , port ) ) ;
_server . Server = server ;
https = cert ! = null ;
this . cert = cert ;
this . protocols = protocols ;
PrintUrls = false ;
}
2022-04-03 15:36:38 +00:00
public HttpServerListener ( IPEndPoint endpoint , IServer server , X509Certificate cert , SslProtocols protocols = SslProtocols . Default )
2022-04-03 14:39:54 +00:00
{
2022-09-01 05:26:26 +00:00
_server = new ChangeableServer ( ) ;
2022-04-03 14:39:54 +00:00
_listener = new TcpListener ( endpoint ) ;
2022-09-01 05:26:26 +00:00
_server . Server = server ;
2022-04-03 14:39:54 +00:00
https = cert ! = null ;
this . cert = cert ;
2022-04-03 15:36:38 +00:00
this . protocols = protocols ;
PrintUrls = false ;
2022-04-02 18:59:12 +00:00
}
2022-09-01 05:26:26 +00:00
public void Listen ( )
{
ListenAsync ( ) . Wait ( ) ;
}
public void Listen ( CancellationToken token )
{
ListenAsync ( token ) . Wait ( ) ;
}
public async Task ListenAsync ( )
{
await ListenAsync ( CancellationToken . None ) ;
}
2022-04-02 18:59:12 +00:00
public async Task ListenAsync ( CancellationToken token )
{
2022-09-01 05:26:26 +00:00
2022-04-02 18:59:12 +00:00
_listener . Start ( ) ;
using ( var r = token . Register ( ( ) = > _listener . Stop ( ) ) ) {
while ( ! token . IsCancellationRequested )
{
2022-05-03 18:56:14 +00:00
try {
2022-04-02 18:59:12 +00:00
var socket = await _listener . AcceptTcpClientAsync ( ) ;
2022-07-06 21:19:40 +00:00
Task . Factory . StartNew ( async ( ) = > {
try {
2022-09-01 05:26:26 +00:00
await CommunicateHostAsync ( socket , ( ) = > {
return socket . Connected ;
} ) ;
2022-07-06 21:19:40 +00:00
} catch ( Exception ex )
{
_ = ex ;
}
} ) . Wait ( 0 ) ;
2022-05-03 18:56:14 +00:00
} catch ( Exception ex )
{
_ = ex ;
}
2022-04-02 18:59:12 +00:00
}
}
}
2022-05-16 20:57:46 +00:00
public async Task PushAsync ( Stream strm , EndPoint local , EndPoint remote )
2022-09-01 05:26:26 +00:00
{
await PushAsync ( strm , local , remote , null ) ;
}
public async Task PushAsync ( Stream strm , EndPoint local , EndPoint remote , Func < bool > isConnected )
2022-05-16 20:57:46 +00:00
{
string request_line = "" ;
string res = ReadHeaders ( strm ) ;
var headers = Headers ( res , out request_line ) ;
// {Method} {Path} HTTP/1.1
ServerContext ctx = null ;
string [ ] request = request_line . Split ( new char [ ] { ' ' } , 3 ) ;
string method = request [ 0 ] ;
try
{
string path = request [ 1 ] ;
string ver = request [ 2 ] ;
2022-09-01 05:26:26 +00:00
ctx = new ServerContext ( method , strm , path , headers , isConnected ) ;
2022-05-16 20:57:46 +00:00
ctx . Server = local as IPEndPoint ;
ctx . Client = remote as IPEndPoint ;
_server . AddCors ( ctx ) ;
if ( PrintUrls )
{
Console . WriteLine ( path ) ;
}
if ( ! await _server . BeforeAsync ( ctx ) )
{
switch ( method )
{
case "HEAD" :
case "GET" :
await _server . GetAsync ( ctx ) ;
break ;
case "POST" :
await _server . PostAsync ( ctx ) ;
break ;
case "OPTIONS" :
await _server . OptionsAsync ( ctx ) ;
break ;
default :
await _server . OtherAsync ( ctx ) ;
break ;
}
}
} catch ( Exception ex )
{
try
{
await ctx . SendExceptionAsync ( ex ) ;
} catch ( Exception ex2 )
{
_ = ex2 ;
}
}
}
public async Task ListenAsync ( CancellationToken token , Action < IPEndPoint > endpoint )
{
_listener . Start ( ) ;
if ( endpoint ! = null )
{
endpoint ( ( IPEndPoint ) _listener . LocalEndpoint ) ;
}
using ( var r = token . Register ( ( ) = > _listener . Stop ( ) ) ) {
while ( ! token . IsCancellationRequested )
{
try {
var socket = await _listener . AcceptTcpClientAsync ( ) ;
2022-09-01 05:26:26 +00:00
await CommunicateHostAsync ( socket , ( ) = > {
return socket . Connected ;
} ) . ConfigureAwait ( false ) ;
2022-05-16 20:57:46 +00:00
} catch ( Exception ex )
{
_ = ex ;
}
}
}
}
2022-04-02 18:59:12 +00:00
2022-04-03 14:39:54 +00:00
private string ReadHeaders ( Stream strm )
2022-04-02 18:59:12 +00:00
{
StringBuilder s = new StringBuilder ( ) ;
2024-03-18 12:31:51 +00:00
2022-04-02 18:59:12 +00:00
while ( ! s . EndsWith ( "\r\n\r\n" , StringComparison . Ordinal ) )
{
int data = strm . ReadByte ( ) ;
if ( data = = - 1 )
{
break ;
}
2024-03-18 12:31:51 +00:00
if ( data > 127 ) continue ;
s . Append ( ( char ) data ) ; //its ascii now
2022-04-02 18:59:12 +00:00
}
return s . ToString ( ) ;
}
private Dictionary < string , List < string > > Headers ( string s , out string req_line )
{
Dictionary < string , List < string > > items = new Dictionary < string , List < string > > ( ) ;
string [ ] lines = s . Split ( new string [ ] { "\r\n" } , StringSplitOptions . RemoveEmptyEntries ) ;
req_line = lines [ 0 ] ;
for ( int i = 1 ; i < lines . Length ; i + + )
{
var line_split = lines [ i ] . Split ( new [ ] { ": " } , 2 , StringSplitOptions . None ) ;
if ( line_split . Length = = 2 )
{
items . Add ( line_split [ 0 ] , line_split [ 1 ] ) ;
}
}
return items ;
}
2022-04-03 14:39:54 +00:00
public Stream GetStream ( TcpClient clt )
{
if ( https )
{
SslStream sslStream = new SslStream (
clt . GetStream ( ) , false ) ;
try
{
2022-04-03 15:36:38 +00:00
sslStream . AuthenticateAsServer ( cert , false , protocols , true ) ;
2022-04-03 14:39:54 +00:00
}
catch ( Exception ex )
{
_ = ex ;
}
return sslStream ;
}
return clt . GetStream ( ) ;
}
2022-09-01 05:26:26 +00:00
private async Task CommunicateHostAsync ( TcpClient clt , Func < bool > isConnected )
2022-04-02 18:59:12 +00:00
{
2022-05-05 17:49:34 +00:00
try {
2022-04-02 18:59:12 +00:00
//<METHOD> <PATH> HTTP/1.1\r\n
//HEADER1\r\n
//HEADER2\r\n
//......
//HEADERN\r\n
//\r\n
//OPTIONAL REQUEST BODY
//RESPONSE
2022-04-03 14:39:54 +00:00
using ( Stream strm = GetStream ( clt ) )
2022-04-02 18:59:12 +00:00
{
2022-09-01 05:26:26 +00:00
await PushAsync ( strm , clt . Client . LocalEndPoint , clt . Client . RemoteEndPoint , isConnected ) ;
2022-04-02 18:59:12 +00:00
}
2022-05-05 17:49:34 +00:00
} catch ( Exception ex )
{
_ = ex ;
}
2022-04-02 18:59:12 +00:00
}
//protected abstract Task<IResult> Get(string url,Dictionary<string,string> headers);
//protected abstract Task GetAsync(ServerContext ctx);
}
}