diff --git a/Tesses.WebServer.NetStandard/ServerContext.cs b/Tesses.WebServer.NetStandard/ServerContext.cs index 981f4a6..67f746e 100644 --- a/Tesses.WebServer.NetStandard/ServerContext.cs +++ b/Tesses.WebServer.NetStandard/ServerContext.cs @@ -79,6 +79,53 @@ internal class SizedStream : Stream /// Method (ex GET, POST, HEAD) /// public string Method { get; set; } + + Func isConnected; + + public bool Connected { + get{ + if(isConnected != null) + { + return isConnected(); + } + return true; + } + } + public ServerContext(string method,Stream strm,string path,Dictionary> headers,Func isConnected) + { + Method = method; + NetworkStream = strm; + RequestHeaders = headers; + ResponseHeaders = new Dictionary>(); + QueryParams = new Dictionary>(); + ResponseHeaders.Add("Server","Tesses.WebServer"); + ResponseHeaders.Add("Connection","close"); + RawUrl=path; + StatusCode = 200; + + // /joel/path/luigi?local=jim&john_surname=connor&demi_surname=lovato&local=tim + + string[] splitUrl = path.Split(new char[] { '?' }, 2); + + if (splitUrl.Length > 0) + { + UrlPath = splitUrl[0]; + OriginalUrlPath=splitUrl[0]; + if (splitUrl.Length == 2) + { + //local=jim&john_surname=connor&demi_surname=lovato&local=tim + //we want to split on & + q_parm = splitUrl[1]; + } + else + { + q_parm = ""; + } + ResetQueryParms(); + } + + this.isConnected=isConnected; + } public ServerContext(string method,Stream strm,string path,Dictionary> headers) { Method = method; @@ -111,7 +158,7 @@ internal class SizedStream : Stream } ResetQueryParms(); } - + isConnected=null; } /// /// Reset query parms (If api sets them) diff --git a/Tesses.WebServer.NetStandard/SimpleHttpCode.cs b/Tesses.WebServer.NetStandard/SimpleHttpCode.cs index 4368900..c3ca0a0 100644 --- a/Tesses.WebServer.NetStandard/SimpleHttpCode.cs +++ b/Tesses.WebServer.NetStandard/SimpleHttpCode.cs @@ -78,8 +78,97 @@ namespace Tesses.WebServer return str; } - + public static void SendNonSeekableStream(this ServerContext ctx,Stream strm,long readFor=-1,string contentType = "application/octet-stream") + { + try + { + long tread=0; + byte[] buffer=new byte[8*1024*1024]; + int read=0; + do + { + if(readFor > -1){ + read=(int)Math.Min(buffer.Length,readFor-tread); + }else{ + read=buffer.Length; + } + if(read == 0) break; + read = strm.Read(buffer,0,read); + strm.Write(buffer,0,read); + }while(read > 0); + } finally { + strm.Close(); + ctx.NetworkStream.Close(); + } + } + public static async Task SendNonSeekableStreamAsync(this ServerContext ctx,Stream strm,long readFor=-1,string contentType="application/octet-stream") + { + try + { + long tread=0; + byte[] buffer=new byte[8*1024*1024]; + int read=0; + do + { + if(readFor > -1){ + read=(int)Math.Min(buffer.Length,readFor-tread); + }else{ + read=buffer.Length; + } + if(read == 0) break; + read = await strm.ReadAsync(buffer,0,read); + await strm.WriteAsync(buffer,0,read); + }while(read > 0); + } finally { + strm.Close(); + ctx.NetworkStream.Close(); + } + } + public static void SendStream(this ServerContext ctx,Stream strm,string contentType="application/octet-stream") + { + //ctx.StatusCode = 200; + int start = 0, end = (int)strm.Length - 1; + if (ctx.RequestHeaders.ContainsKey(BYTES_RANGE_HEADER) && strm.CanSeek) + { + if (ctx.RequestHeaders[BYTES_RANGE_HEADER].Count > 1) + { + throw new NotSupportedException("Multiple 'Range' headers are not supported."); + } + var range = ctx.RequestHeaders[BYTES_RANGE_HEADER][0].Replace("bytes=", String.Empty) + .Split(new string[] { "-" }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => Int32.Parse(x)) + .ToArray(); + start = (range.Length > 0) ? range[0] : 0; + end = (range.Length > 1) ? range[1] : (int)(strm.Length - 1); + + + var hdrs = ctx.ResponseHeaders; + hdrs.Add("Accept-Ranges", "bytes"); + hdrs.Add("Content-Range", "bytes " + start + "-" + end + "/" + strm.Length); + ctx.StatusCode = 206; + + + } + ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString()); + ctx.ResponseHeaders.Add("Content-Type", contentType); + + ctx.WriteHeaders(); + if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal)) + { + try + { + if(strm.CanSeek) + strm.Position = start; + strm.CopyTo(ctx.NetworkStream, Math.Min(8 * 1024 * 1024, end - start + 1)); + } + finally + { + strm.Close(); + ctx.NetworkStream.Close(); + } + } + } public static async Task SendStreamAsync(this ServerContext ctx, Stream strm, string contentType = "application/octet-stream") { //ctx.StatusCode = 200; @@ -116,7 +205,7 @@ namespace Tesses.WebServer { if(strm.CanSeek) strm.Position = start; - strm.CopyTo(ctx.NetworkStream, Math.Min(8 * 1024 * 1024, end - start + 1)); + await strm.CopyToAsync(ctx.NetworkStream, Math.Min(8 * 1024 * 1024, end - start + 1)); } finally { diff --git a/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj b/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj index b21144d..f0b6b03 100644 --- a/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj +++ b/Tesses.WebServer.NetStandard/Tesses.WebServer.NetStandard.csproj @@ -5,9 +5,9 @@ Tesses.WebServer Mike Nolan Tesses - 1.0.3.7 - 1.0.3.7 - 1.0.3.7 + 1.0.3.8 + 1.0.3.8 + 1.0.3.8 A TCP Listener HTTP(s) Server MIT HTTP, WebServer, Website diff --git a/Tesses.WebServer.NetStandard/TessesServer.cs b/Tesses.WebServer.NetStandard/TessesServer.cs index 17818ba..861861e 100644 --- a/Tesses.WebServer.NetStandard/TessesServer.cs +++ b/Tesses.WebServer.NetStandard/TessesServer.cs @@ -15,9 +15,63 @@ using System.Security.Authentication; 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 /// @@ -26,7 +80,7 @@ namespace Tesses.WebServer public static async Task ReadStringAsync(this ServerContext ctx) { string str = null; - using (var reader = new StreamReader(ctx.NetworkStream)) + using (var reader = new StreamReader(ctx.GetRequestStream())) { str = await reader.ReadToEndAsync(); } @@ -34,6 +88,21 @@ namespace Tesses.WebServer 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 @@ -45,6 +114,17 @@ namespace Tesses.WebServer 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 @@ -54,6 +134,17 @@ namespace Tesses.WebServer 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 @@ -63,7 +154,17 @@ namespace Tesses.WebServer public static async Task ReadToStreamAsync(this ServerContext ctx,Stream strm) { - await ctx.NetworkStream.CopyToAsync(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 @@ -84,10 +185,33 @@ namespace Tesses.WebServer } using(var f = File.Create(filename)) { - await ctx.NetworkStream.CopyToAsync(f); + 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 @@ -110,6 +234,26 @@ namespace Tesses.WebServer 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 @@ -121,6 +265,19 @@ namespace Tesses.WebServer { 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 @@ -136,6 +293,19 @@ namespace Tesses.WebServer 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 @@ -145,6 +315,15 @@ namespace Tesses.WebServer 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 @@ -156,6 +335,17 @@ namespace Tesses.WebServer 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 @@ -168,6 +358,18 @@ namespace Tesses.WebServer 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 @@ -181,6 +383,19 @@ namespace Tesses.WebServer } } /// + /// 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 @@ -461,7 +676,7 @@ namespace Tesses.WebServer { ctx.StatusCode = (int)HttpStatusCode.MethodNotAllowed; await ctx.SendTextAsync("Method Not Supported"); - + } /// /// Called on OPTIONS Request @@ -839,35 +1054,96 @@ namespace Tesses.WebServer public bool PrintUrls {get;set;} bool https; X509Certificate cert; - IServer _server; + + 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.Server=server; https = false; PrintUrls=false; } public HttpServerListener(IServer server) { + _server=new ChangeableServer(); _listener = new TcpListener(new IPEndPoint(IPAddress.Any, 3251)); - _server = server; + _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.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) @@ -876,7 +1152,9 @@ namespace Tesses.WebServer var socket=await _listener.AcceptTcpClientAsync(); Task.Factory.StartNew(async()=>{ try{ - await CommunicateHostAsync(socket); + await CommunicateHostAsync(socket,()=>{ + return socket.Connected; + }); }catch(Exception ex) { _=ex; @@ -891,6 +1169,10 @@ namespace Tesses.WebServer } } 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); @@ -904,7 +1186,7 @@ namespace Tesses.WebServer { string path = request[1]; string ver = request[2]; - ctx = new ServerContext(method, strm, path, headers); + ctx = new ServerContext(method, strm, path, headers,isConnected); ctx.Server =local as IPEndPoint; ctx.Client = remote as IPEndPoint; _server.AddCors(ctx); @@ -956,7 +1238,9 @@ namespace Tesses.WebServer { try{ var socket=await _listener.AcceptTcpClientAsync(); - await CommunicateHostAsync(socket).ConfigureAwait(false); + await CommunicateHostAsync(socket,()=>{ + return socket.Connected; + }).ConfigureAwait(false); }catch(Exception ex) { _=ex; @@ -1023,7 +1307,7 @@ namespace Tesses.WebServer } return clt.GetStream(); } - private async Task CommunicateHostAsync(TcpClient clt) + private async Task CommunicateHostAsync(TcpClient clt,Func isConnected) { try{ // HTTP/1.1\r\n @@ -1039,7 +1323,7 @@ namespace Tesses.WebServer using (Stream strm = GetStream(clt)) { - await PushAsync(strm,clt.Client.LocalEndPoint,clt.Client.RemoteEndPoint); + await PushAsync(strm,clt.Client.LocalEndPoint,clt.Client.RemoteEndPoint,isConnected); } }catch(Exception ex) {