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; using System.Security.Cryptography.X509Certificates; using System.Net.Security; using System.Security.Authentication; using System.Web; using Tesses.VirtualFilesystem; namespace Tesses.WebServer { internal class SendEventArgs : EventArgs { public string Data {get;set;} } public class SendEvents { internal event EventHandler 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; } } } public static class Extensions { 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 cb= (sender,e0)=>{ if(__connected) ctx.NetworkStream.Write($"data: {e0.Data}\n\n"); }; evt.EventReceived += cb; while(ctx.Connected); evt.EventReceived -= cb; __connected=false; }catch(Exception ex) { _=ex; } } /// /// Read string from request body /// /// ServerContext /// the contents of request public static async Task ReadStringAsync(this ServerContext ctx) { string str = null; using (var reader = new StreamReader(ctx.GetRequestStream())) { str = await reader.ReadToEndAsync(); } return str; } /// /// Read string from request body /// /// ServerContext /// the contents of request public static string ReadString(this ServerContext ctx) { string str = null; using (var reader = new StreamReader(ctx.GetRequestStream())) { str = reader.ReadToEnd(); } return str; } /// /// Read json from request body /// /// ServerContext /// type of object (for scema) /// object of type T with deserialized json data public static async Task ReadJsonAsync(this ServerContext ctx) { var json=await ctx.ReadStringAsync(); return JsonConvert.DeserializeObject(json); } /// /// Read json from request body /// /// ServerContext /// type of object (for scema) /// object of type T with deserialized json data public static T ReadJson(this ServerContext ctx) { var json= ctx.ReadString(); return JsonConvert.DeserializeObject(json); } /// /// Read request body to array /// /// ServerContext /// Request body data public static async Task ReadBytesAsync(this ServerContext ctx) { MemoryStream strm = new MemoryStream(); await ctx.ReadToStreamAsync(strm); return strm.ToArray(); } /// /// Read request body to array /// /// ServerContext /// Request body data public static byte[] ReadBytes(this ServerContext ctx) { MemoryStream strm = new MemoryStream(); ctx.ReadToStream(strm); return strm.ToArray(); } /// /// Read request body to stream /// /// ServerContext /// Stream to write to public static async Task ReadToStreamAsync(this ServerContext ctx,Stream strm) { await ctx.GetRequestStream().CopyToAsync(strm); } /// /// Read request body to stream /// /// ServerContext /// Stream to write to public static void ReadToStream(this ServerContext ctx,Stream strm) { ctx.GetRequestStream().CopyTo(strm); } /// /// Read request body to file /// /// ServerContext /// name of file to write too, can be without extension /// file path with extension unless mimetype header is missing public static async Task 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)) { await ctx.ReadToStreamAsync(f); } return filename; } /// /// Read request body to file /// /// ServerContext /// name of file to write too, can be without extension /// file path with extension unless mimetype header is missing 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; } /// /// Write headers to stream /// /// ServerContext public static async Task WriteHeadersAsync(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()); await ctx.NetworkStream.WriteAsync(data, 0, data.Length); } /// /// Write headers to stream /// /// ServerContext 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); } /// /// Send file to client (supports range partial content) /// /// ServerContext /// the file to serve public static async Task SendFileAsync(this ServerContext ctx, string file) { using (var strm = File.OpenRead(file)) { await ctx.SendStreamAsync( strm, MimeTypesMap.GetMimeType(file)); } } /// /// Send file to client (supports range partial content) /// /// ServerContext /// the file to serve public static void SendFile(this ServerContext ctx, string file) { using (var strm = File.OpenRead(file)) { ctx.SendStream( strm, MimeTypesMap.GetMimeType(file)); } } /// /// Send exception to client /// /// ServerContext /// the Exception public static async Task SendExceptionAsync(this ServerContext ctx, Exception ex) { string name = ex.GetType().FullName; string j = $"{WebUtility.HtmlEncode(name)} thrown

{WebUtility.HtmlEncode(name)} thrown

Description: {WebUtility.HtmlEncode(ex.Message)}

"; ctx.StatusCode = 500; await ctx.SendTextAsync(j); } /// /// Send exception to client /// /// ServerContext /// the Exception public static void SendException(this ServerContext ctx, Exception ex) { string name = ex.GetType().FullName; string j = $"{WebUtility.HtmlEncode(name)} thrown

{WebUtility.HtmlEncode(name)} thrown

Description: {WebUtility.HtmlEncode(ex.Message)}

"; ctx.StatusCode = 500; ctx.SendText(j); } /// /// Send object as json to client /// /// ServerContext /// an object to serialize with newtonsoft.json public static async Task SendJsonAsync(this ServerContext ctx,object value) { await ctx.SendTextAsync(JsonConvert.SerializeObject(value), "application/json"); } /// /// Send object as json to client /// /// ServerContext /// an object to serialize with newtonsoft.json public static void SendJson(this ServerContext ctx,object value) { ctx.SendText(JsonConvert.SerializeObject(value), "application/json"); } /// /// Send text to client /// /// ServerContext /// some text /// mime type public static async Task SendTextAsync(this ServerContext ctx, string data, string contentType = "text/html") { await ctx.SendBytesAsync(Encoding.UTF8.GetBytes(data), contentType); } /// /// Send text to client /// /// ServerContext /// some text /// mime type public static void SendText(this ServerContext ctx, string data, string contentType = "text/html") { ctx.SendBytes(Encoding.UTF8.GetBytes(data), contentType); } /// /// Send redirect /// /// ServerContext /// Url to redirect to 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(); } /// /// Send redirect /// /// ServerContext /// Url to redirect to 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(); } /// /// Send byte[] to client /// /// ServerContext /// a byte[] array /// mime type public static async Task SendBytesAsync(this ServerContext ctx, byte[] array, string contentType = "application/octet-stream") { using (var ms = new MemoryStream(array)) { await ctx.SendStreamAsync( ms, contentType); } } /// /// Send byte[] to client /// /// ServerContext /// a byte[] array /// mime type public static void SendBytes(this ServerContext ctx, byte[] array, string contentType = "application/octet-stream") { using (var ms = new MemoryStream(array)) { ctx.SendStream( ms, contentType); } } /// /// Get first item in Dictionary> based on key /// /// the dictionary with list value /// some key /// key type /// value type /// public static T2 GetFirst(this Dictionary> args,T1 key) { return args[key][0]; } /// /// Try to get first item in Dictionary> based on key /// /// the dictionary with list value /// the key to check /// the value returned /// key type /// value type /// true if found else false if not found public static bool TryGetFirst(this Dictionary> args,T1 key,out T2 value) { List ls; if (args.TryGetValue(key,out ls)) { if(ls.Count > 0) { value = ls[0]; return true; } } value = default(T2); return false; } /// /// Add item to the Dictionary> with specified key (will create key in dictionary if not exist) /// /// the dictionary with list value /// the key to add or to add to /// a item /// key type /// value type public static void Add(this Dictionary> list,T1 key,T2 item) { if (list.ContainsKey(key)) { list[key].Add(item); } else { List items = new List(); items.Add(item); list.Add(key, items); } } /// /// Add multiple items to the Dictionary> with specified key (will create key in dictionary if not exist) /// /// the dictionary with list value /// the key to add or to add to /// IEnumerable /// key type /// value type public static void AddRange(this Dictionary> list,T1 key,IEnumerable items) { if (list.ContainsKey(key)) { list[key].AddRange(items); } else { List items2 = new List(); items2.AddRange(items); list.Add(key, items2); } } /// /// StringBuilder ends with /// /// string builder /// text to check /// comparison type /// true if sb ends with test, false if it does not 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); } } /// /// returns 404 not found page /// public class NotFoundServer : SameServer { /// /// 404 not found custom html use "{url}" in your html as url /// /// the custom html public NotFoundServer(string html) : base(html) { } /// ///404 not found default html /// public NotFoundServer() : base("File {url} not found

404 Not Found

{url}

") { } } public class SameServer : Server { /// /// html use "{url}" in your html as url /// /// the custom html /// the status code public SameServer(string html,int statusCode=404) { _html=html; _statusCode = statusCode; } int _statusCode; string _html; public override async Task GetAsync(ServerContext ctx) { ctx.StatusCode = _statusCode; await ctx.SendTextAsync( _html.Replace("{url}", WebUtility.HtmlEncode(ctx.OriginalUrlPath))); } } public enum WebServerPathType { 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 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; /// /// construct with filesystem /// /// filesystem for root public WebServerStyleFilesystemHandler(IVirtualFilesystem fs) { _defaultFileNames = new string[] {"index.html","index.htm","default.html","default.htm" }; _fs=fs; } /// /// construct with filesystem, custom filenames /// /// filesystem for root /// like index.html, index.htm, default.html, default.htm public WebServerStyleFilesystemHandler(IVirtualFilesystem fs,string[] defaultFileNames) { _fs = fs; _defaultFileNames = defaultFileNames; } 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) { var _name = Special.Root / path; 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 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 { string[] _defaultFileNames; string _path; /// /// construct with path /// /// directory for root public WebServerStyleFileHandler(string path) { _defaultFileNames = new string[] {"index.html","index.htm","default.html","default.htm" }; _path=path; } /// /// construct with path, custom filenames /// /// directory for root /// like index.html, index.htm, default.html, default.htm public WebServerStyleFileHandler(string path,string[] defaultFileNames) { _path = path; _defaultFileNames = defaultFileNames; } public bool DefaultFileExists(string path,out string name) { foreach(var def in _defaultFileNames) { name = Path.Combine(path, def); if(File.Exists(name)) { return true; } } name = ""; return false; } public WebServerPathEntry GetPath(string url) { string someUrl = Path.Combine(_path,WebUtility.UrlDecode(url.Substring(1)).Replace('/', Path.DirectorySeparatorChar)); //Console.WriteLine(someUrl); if (Directory.Exists(someUrl)) { string name; if(DefaultFileExists(someUrl,out name)) { return new WebServerPathEntry(name,WebServerPathType.File,url); }else{ return new WebServerPathEntry(someUrl,WebServerPathType.Directory,url); } } else if (File.Exists(someUrl)) { return new WebServerPathEntry(someUrl,WebServerPathType.File,url); } else { return new WebServerPathEntry(someUrl,WebServerPathType.NotFound,url); } } public IEnumerable 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)}/"; } 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))); } } public class StaticServerFileHandler : EventArgs { public string RealPath {get;set;} public string CurrentHttpPath {get;set;} public bool HasBeenHandled {get;set;}=false; public ServerContext Context {get;set;} public bool Cancel {get;set;}=false; } public enum UploadAllowedResponse { Yes=0, NoISentResponse=1, No=2 } /// /// Serve static files (doesnt allow listing files) /// public class StaticServer : Server { public bool AllowUpload {get;set;}=false; bool allow=false; public Func 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 FileHandler; IServer _server; IFileHandler fileHandler; IServer _forbidden; /// /// construct with filesystem /// /// filesystem for server public StaticServer(IVirtualFilesystem fs) { _server = new NotFoundServer(); fileHandler=new WebServerStyleFilesystemHandler(fs); _forbidden = new SameServer("File {url} not accessable

403 Forbidden

{url}

",403); } /// /// construct with filesystem, custom filenames, and server for not found /// /// filesystem for server /// like index.html, index.htm, default.html, default.htm /// 404 not found server public StaticServer(IVirtualFilesystem fs,string[] defaultFileNames,IServer notfoundserver) { _server = notfoundserver; fileHandler=new WebServerStyleFilesystemHandler(fs,defaultFileNames); _forbidden = new SameServer("File {url} not accessable

403 Forbidden

{url}

",403); } /// /// construct with filesystem, custom filenames, and server for not found /// /// filesystem for server /// like index.html, index.htm, default.html, default.htm /// 404 not found server /// 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("File {url} not accessable

403 Forbidden

{url}

",403); } /// /// construct with filesystem, custom filenames, and server for not found, server for forbidden and with option to allow/deny listing directories /// /// filesystem for server /// like index.html, index.htm, default.html, default.htm /// 404 not found server /// server for forbidden files/denied listing /// 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; } /// /// construct with filesystem, custom filenames and with option to allow/deny listing directories /// /// filesystem for server /// like index.html, index.htm, default.html, default.htm /// 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("File {url} not accessable

403 Forbidden

{url}

",403); } /// /// construct with filesystem and with option to allow/deny listing directories /// /// filesystem for server /// 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("File {url} not accessable

403 Forbidden

{url}

",403); } /// /// construct with path /// /// directory for server public StaticServer(string path) { _server = new NotFoundServer(); fileHandler=new WebServerStyleFileHandler(path); _forbidden = new SameServer("File {url} not accessable

403 Forbidden

{url}

",403); } /// /// construct with path, custom filenames, and server for not found /// /// directory for server /// like index.html, index.htm, default.html, default.htm /// 404 not found server public StaticServer(string path,string[] defaultFileNames,IServer notfoundserver) { _server = notfoundserver; fileHandler=new WebServerStyleFileHandler(path,defaultFileNames); _forbidden = new SameServer("File {url} not accessable

403 Forbidden

{url}

",403); } /// /// construct with path, custom filenames, and server for not found /// /// directory for server /// like index.html, index.htm, default.html, default.htm /// 404 not found server /// 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("File {url} not accessable

403 Forbidden

{url}

",403); } /// /// construct with path, custom filenames, and server for not found, server for forbidden and with option to allow/deny listing directories /// /// directory for server /// like index.html, index.htm, default.html, default.htm /// 404 not found server /// server for forbidden files/denied listing /// 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; } /// /// construct with path, custom filenames and with option to allow/deny listing directories /// /// directory for server /// like index.html, index.htm, default.html, default.htm /// 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("File {url} not accessable

403 Forbidden

{url}

",403); } /// /// construct with path and with option to allow/deny listing directories /// /// directory for server /// whether to allow listing directory or not (overridable by environment variable with TESSES_WEBSERVER_ALLOW_LISTING=true|false) public bool RedirectToRootInsteadOfNotFound {get;set;}=false; public StaticServer(string path,bool allowListing) { _server = new NotFoundServer(); fileHandler=new WebServerStyleFileHandler(path); AllowListingDirectories = allowListing; _forbidden = new SameServer("File {url} not accessable

403 Forbidden

{url}

",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 dirs=new List(); 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: if(RedirectToRootInsteadOfNotFound) { var fileEntry2 = fileHandler.GetPath("/"); switch(fileEntry.Type) { case WebServerPathType.File: using(var strm = fileHandler.Open(fileEntry2)) await ctx.SendStreamAsync(strm,HeyRed.Mime.MimeTypesMap.GetMimeType(fileEntry.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; } }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 entries) { /*if(!this.ln.Path.EndsWith("/")) { SendRedirect(this.ln.Path + '/'); return; }*/ StringBuilder b=new StringBuilder(); b.Append($"{HttpUtility.HtmlEncode(title)}

{HttpUtility.HtmlEncode(title)}

{HttpUtility.HtmlEncode(desc)}

../
"); foreach(var entry in entries) { //{HttpUtility.HtmlEncode(_getFileNameWithoutQuery(entry))}
"); } b.Append("

Tesses.WebServer
"); return b.ToString(); } public string Title {get;set;}="Directory Listing"; public string Description {get;set;}=""; public void FromEnumerable(string title,string desc,IEnumerable entries) { curText = GetEnumerable(title,desc,entries); } public void FromDirectory(string title,string desc,string directory,string reqPath) { List items=new List(); 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); } } /// /// Server where you can change inner server /// public class ChangeableServer : Server { /// /// The inner server to change /// public IServer Server {get;set;} /// /// Construct with default value /// public ChangeableServer() { Server=null; } /// /// Construct with server /// /// the inner server public ChangeableServer(IServer svr) { Server=svr; } public override async Task 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); } } /// /// Abstract class for server /// public abstract class Server : IServer { /// /// Returns 404 Not found /// public static readonly NotFoundServer ServerNull = new NotFoundServer(); /// /// You are guarenteed to have a server /// /// any server object /// if null return ServerNull otherwise return svr public IServer Guaranteed(IServer svr) { if(svr != null) { return svr; } return ServerNull; } /// /// Put cors header /// public bool CorsHeader = true; /// /// Called on GET Request /// /// ServerContext public abstract Task GetAsync(ServerContext ctx); /// /// Called on POST Request /// /// ServerContext public virtual async Task PostAsync(ServerContext ctx) { ctx.StatusCode = (int)HttpStatusCode.MethodNotAllowed; await ctx.SendTextAsync("Method Not Supported"); } /// /// Called on OPTIONS Request /// /// ServerContext public virtual async Task OptionsAsync(ServerContext ctx) { await ctx.WriteHeadersAsync(); ctx.NetworkStream.Close(); } /// /// Called on any other Request method /// /// ServerContext public virtual async Task OtherAsync(ServerContext ctx) { ctx.StatusCode = (int)HttpStatusCode.MethodNotAllowed; await ctx.SendTextAsync("Method Not Supported"); } /// /// Called before request was made /// /// ServerContext /// true to cancel request, false to continue request public virtual async Task BeforeAsync(ServerContext ctx) { return await Task.FromResult(false); } /// /// Add cors header /// /// Server Context 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"); } } } /// /// mount multiple servers at different url paths /// public sealed class MountableServer : Server { Dictionary _servers = new Dictionary(); public MountableServer(IServer root) { _root = root; } IServer _root; private (string Key,IServer Value) GetFromPath(ServerContext ctx) { //bool j = false; foreach(var item in _servers.Reverse()) { if(ctx.UrlPath.StartsWith(item.Key,StringComparison.Ordinal)) { return (item.Key,Guaranteed(item.Value)); } if (ctx.UrlPath == item.Key.TrimEnd('/')) { ctx.UrlPath += "/"; return (item.Key,Guaranteed(item.Value)); } } //Console.WriteLine("HERE WE ARE"); return ("/",Guaranteed(_root)); } /// /// 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 /// /// URL. /// Server. public void Mount(string url,IServer server) { _servers.Add(url, server); } /// /// Unmount a server /// /// Url public void Unmount(string url) { _servers.Remove(url); } /// /// Unmount all servers /// 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 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); } } /// /// Check username and password are correct or if request can be anonymous /// /// Username, can and will be "" on first request for resource /// Password, can and will be "" on first request for resource /// true for authorized, false for unauthorized public delegate bool Authenticate(string username, string password); /// /// Check username and password are correct or if request can be anonymous /// /// Server Context /// Username, can and will be "" on first request for resource /// Password, can and will be "" on first request for resource /// true for authorized, false for unauthorized public delegate bool AuthenticateWithContext(ServerContext context,string username,string password); /// /// Protect server with password /// public class BasicAuthServer : Server { /// /// Construct server for user authorization /// /// callback for authorization /// server to protect /// realm parameter in WWW-Auhenticate Header public BasicAuthServer(Authenticate auth,IServer inner,string realm="SampleRealm") { Authenticate = auth; InnerServer = inner; Realm = realm; } /// /// Construct server for user authorization (With ServerContext in callback) /// /// callback for authorization /// server to protect /// realm parameter in WWW-Auhenticate Header public BasicAuthServer(AuthenticateWithContext auth,IServer inner,string realm = "SampleRealm") { AuthenticateWithContext=auth; InnerServer=inner; Realm = realm; } public override async Task 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); } /// /// Server to protect /// public IServer InnerServer { get; set; } /// /// Authentication callback without ServerContext /// public Authenticate Authenticate { get; set; } /// /// Authentication callback with ServerContext /// public AuthenticateWithContext AuthenticateWithContext {get;set;} /// /// Realm parameter in WWW-Authenticate header /// public string Realm { get; set; } private bool ValidAuth(ServerContext ctx) { string auth; if(Authenticate == null && AuthenticateWithContext == null) return true; 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); if(Authenticate != null) return Authenticate(userPass[0], userPass[1]); if(AuthenticateWithContext != null) return AuthenticateWithContext(ctx,userPass[0],userPass[2]); } }else{ if(Authenticate != null) return Authenticate("", ""); if(AuthenticateWithContext != null) return AuthenticateWithContext(ctx,"",""); } return false; } private async Task Authorize(ServerContext ctx) { if (Authenticate == null && AuthenticateWithContext == null) 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(); } public void Clear() { Servers.Clear(); } public void Remove(string fqdn_or_ip) { Servers.Remove(fqdn_or_ip); } public IServer Default { get; set; } Dictionary Servers; public void AddDomain(string fqdn_or_ip,IServer svr) { Servers.Add(fqdn_or_ip, svr); } public override async Task 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); } } public interface IServer { void AddCors(ServerContext ctx); Task BeforeAsync(ServerContext ctx); Task GetAsync(ServerContext ctx); Task PostAsync(ServerContext ctx); Task OptionsAsync(ServerContext ctx); Task OtherAsync(ServerContext ctx); } public sealed class HttpServerListener { /// /// Print urls when running /// /// true if verbose, false if not public bool PrintUrls {get;set;} bool https; X509Certificate cert; ChangeableServer _server; TcpListener _listener; SslProtocols protocols; 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; } public HttpServerListener(int port,IServer server) { _server=new ChangeableServer(); _listener = new TcpListener(new IPEndPoint(IPAddress.Any,port)); _server.Server=server; https = false; PrintUrls=false; } public HttpServerListener(IPEndPoint endPoint,IServer server) { _server=new ChangeableServer(); _listener = new TcpListener(endPoint); _server.Server=server; https = false; PrintUrls=false; } public HttpServerListener(IServer server) { _server=new ChangeableServer(); _listener = new TcpListener(new IPEndPoint(IPAddress.Any, 3251)); _server.Server = server; https = false; PrintUrls=false; } public HttpServerListener() { _server=new ChangeableServer(); _listener = new TcpListener(new IPEndPoint(IPAddress.Any, 3251)); https = false; PrintUrls=false; } 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; } public HttpServerListener(IPEndPoint endpoint,IServer server,X509Certificate cert,SslProtocols protocols=SslProtocols.Default) { _server=new ChangeableServer(); _listener = new TcpListener(endpoint); _server.Server = server; https = cert != null; this.cert = cert; this.protocols=protocols; PrintUrls=false; } public void Listen() { ListenAsync().Wait(); } public void Listen(CancellationToken token) { ListenAsync(token).Wait(); } public async Task ListenAsync() { await ListenAsync(CancellationToken.None); } public async Task ListenAsync(CancellationToken token) { _listener.Start(); using (var r = token.Register(() => _listener.Stop())) { while (!token.IsCancellationRequested) { try{ var socket=await _listener.AcceptTcpClientAsync(); Task.Factory.StartNew(async()=>{ try{ await CommunicateHostAsync(socket,()=>{ return socket.Connected; }); }catch(Exception ex) { _=ex; } }).Wait(0); }catch(Exception ex) { _=ex; } } } } public async Task PushAsync(Stream strm,EndPoint local,EndPoint remote) { await PushAsync(strm,local,remote,null); } public async Task PushAsync(Stream strm,EndPoint local,EndPoint remote,Func isConnected) { 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]; ctx = new ServerContext(method, strm, path, headers,isConnected); 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 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(); await CommunicateHostAsync(socket,()=>{ return socket.Connected; }).ConfigureAwait(false); }catch(Exception ex) { _=ex; } } } } private string ReadHeaders(Stream strm) { StringBuilder s = new StringBuilder(); var decoder = Encoding.UTF8.GetDecoder(); var nextChar = new char[1]; while (!s.EndsWith("\r\n\r\n",StringComparison.Ordinal)) { int data = strm.ReadByte(); if(data == -1) { break; } int charCount=decoder.GetChars(new byte[] { (byte)data }, 0, 1, nextChar, 0); if (charCount == 0) continue; s.Append(nextChar); } return s.ToString(); } private Dictionary> Headers(string s,out string req_line) { Dictionary> items = new Dictionary>(); string[] lines = s.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); req_line = lines[0]; for(int i=1;i isConnected) { try{ // HTTP/1.1\r\n //HEADER1\r\n //HEADER2\r\n //...... //HEADERN\r\n //\r\n //OPTIONAL REQUEST BODY //RESPONSE using (Stream strm = GetStream(clt)) { await PushAsync(strm,clt.Client.LocalEndPoint,clt.Client.RemoteEndPoint,isConnected); } }catch(Exception ex) { _=ex; } } //protected abstract Task Get(string url,Dictionary headers); //protected abstract Task GetAsync(ServerContext ctx); } }