From 79b3eabde69b04408943116a489b18d289c9f0ea Mon Sep 17 00:00:00 2001 From: Harald Wolff Date: Mon, 4 Nov 2019 10:00:33 +0100 Subject: [PATCH] WIP --- HTTPServer.cs | 150 ++++++++++++++++----------------- HttpReader.cs | 11 ++- HttpRequest.cs | 9 +- HttpResponse.cs | 11 ++- cert/CertContainer.cs | 59 +++++++++++++ connections/Connection.cs | 80 ++++++++++++++++++ connections/HttpConnection.cs | 37 ++++++++ connections/HttpsConnection.cs | 42 +++++++++ listener/HttpListener.cs | 55 ++++++++++++ listener/HttpsListener.cs | 30 +++++++ listener/Listener.cs | 44 ++++++++++ ln.http.csproj | 11 +++ websocket/WebSocket.cs | 35 ++++---- 13 files changed, 466 insertions(+), 108 deletions(-) create mode 100644 cert/CertContainer.cs create mode 100644 connections/Connection.cs create mode 100644 connections/HttpConnection.cs create mode 100644 connections/HttpsConnection.cs create mode 100644 listener/HttpListener.cs create mode 100644 listener/HttpsListener.cs create mode 100644 listener/Listener.cs diff --git a/HTTPServer.cs b/HTTPServer.cs index 2e25af1..1ea495a 100644 --- a/HTTPServer.cs +++ b/HTTPServer.cs @@ -1,14 +1,15 @@ using System; using System.Collections.Generic; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.IO; using ln.http.resources.session; using ln.logging; using ln.types.threads; using ln.types; +using ln.http.listener; +using ln.http.connections; +using ln.types.net; +using ln.http.cert; using System.Globalization; +using System.Net.Sockets; namespace ln.http @@ -20,53 +21,59 @@ namespace ln.http public static bool exclusivePortListener = false; public HttpApplication DefaultApplication { get; set; } - public SessionCache SessionCache { get; set; } - public bool IsRunning => (currentListenerThreads.Count > 0) || (threadPool.CurrentPoolSize > 0); - - public Func CreateServerConnection { get; set; } + public bool IsRunning => !shutdown && (threadPool.CurrentPoolSize > 0); + public Func CreateServerConnection { get; set; } public Logger Logger { get; set; } - bool shutdown = false; + List listeners = new List(); + public Listener[] Listeners => listeners.ToArray(); - - Dictionary tcpListeners = new Dictionary(); Dictionary applications = new Dictionary(); - Dictionary currentListenerThreads = new Dictionary(); - DynamicPool threadPool; public DynamicPool ThreadPool => threadPool; public HTTPServer() { Logger = Logger.Default; - CreateServerConnection = (HTTPServer httpServer, TcpClient tcpClient) => new HTTPServerConnection(httpServer, tcpClient); SessionCache = new SessionCache(); - threadPool = new DynamicPool(1024); } - public void AddEndpoint(IPEndPoint endpoint) + public void AddListener(Listener listener) { - if (this.tcpListeners.ContainsKey(endpoint)) - { - throw new ArgumentOutOfRangeException(nameof(endpoint), "EndPoint already added"); - } - - this.tcpListeners.Add(endpoint, new TcpListener(endpoint)); + listeners.Add(listener); + if (IsRunning) + StartListener(listener); } - public void RemoveEndpoint(IPEndPoint endpoint) + + public void StartListener(Listener listener) { - if (this.tcpListeners.ContainsKey(endpoint)) - { - this.tcpListeners[endpoint].Stop(); - this.tcpListeners.Remove(endpoint); - } + listener.Open(); + + threadPool.Enqueue( + () => listener.AcceptMany( + (connection) => threadPool.Enqueue( + () => this.HandleConnection(connection) + ) + ) + ); + } + + public void StopListener(Listener listener) + { + listener.Close(); + } + + + public void AddEndpoint(Endpoint endpoint) + { + AddListener(new HttpListener(endpoint)); } public void AddApplication(URI BaseURI, HttpApplication application) @@ -89,13 +96,8 @@ namespace ln.http { threadPool.Start(); - foreach (TcpListener tcpListener in this.tcpListeners.Values) - { - tcpListener.Start(backlog); - - Thread listenerThread = new Thread(() => AcceptConnections(tcpListener)); - listenerThread.Start(); - } + foreach (Listener listener in listeners) + StartListener(listener); } public void Stop() @@ -104,64 +106,54 @@ namespace ln.http { this.shutdown = true; } + foreach (Listener listener in listeners) + StopListener(listener); - foreach (TcpListener tcpListener in tcpListeners.Values) - { - tcpListener.Stop(); - } - - foreach (HTTPServerConnection connection in HTTPServerConnection.CurrentConnections) - connection?.Abort(); - - if (threadPool.State == PoolState.RUN) - threadPool.Stop(true); + threadPool.Stop(true); } - private void AcceptConnections(TcpListener tcpListener) + private void HandleConnection(Connection connection) { - lock (this) - { - if (currentListenerThreads.ContainsKey(tcpListener)) - { - throw new Exception("HTTP listener thread already running"); - } - currentListenerThreads.Add(tcpListener, Thread.CurrentThread); - } - + HttpRequest httpRequest = null; + bool keepAlive = true; try { - - while (!this.shutdown) + do { - AcceptConnection(tcpListener); - } + httpRequest = connection.ReadRequest(this); + if (httpRequest == null) + break; + httpRequest.ApplySession(SessionCache); + + HttpApplication application = GetHttpApplication(new URI(httpRequest.BaseURI.ToString())); + + application.Authenticate(httpRequest); + application.Authorize(httpRequest); + + HttpResponse response = application.GetResponse(httpRequest); + + keepAlive = httpRequest.GetRequestHeader("connection","keep-alive").Equals("keep-alive") && response.GetHeader("connection", "keep-alive").Equals("keep-alive"); + response.SetHeader("connection", keepAlive ? "keep-alive" : "close"); + + connection.SendResponse(response); + + } while (keepAlive); } catch (Exception e) { - Logging.Log(LogLevel.ERROR, "HTTPServer: Listener thread caught exception {0}", e); Logging.Log(e); + if (httpRequest != null) + { + HttpResponse httpResponse = new HttpResponse(httpRequest); + httpResponse.StatusCode = 500; + httpResponse.ContentWriter.WriteLine("500 Internal Server Error"); + + connection.SendResponse(httpResponse); + } } - lock (this) - { - currentListenerThreads.Remove(tcpListener); - } - } - - private void AcceptConnection(TcpListener tcpListener) - { - TcpClient tcpClient = tcpListener.AcceptTcpClient(); - - HTTPServerConnection connection = CreateServerConnection(this, tcpClient); - if (connection == null) - { - Logging.Log(LogLevel.ERROR, "HTTPServer: CreateServerConnection(): returned null"); - } - else - { - this.threadPool.Enqueue(connection); - } + connection.GetStream().Close(); } public void Log(DateTime startTime,double duration,HttpRequest httpRequest,HttpResponse httpResponse) diff --git a/HttpReader.cs b/HttpReader.cs index f0bf80f..a79acc4 100644 --- a/HttpReader.cs +++ b/HttpReader.cs @@ -3,6 +3,7 @@ using System.IO; using System.Text; using System.Collections.Generic; using System.Net; +using ln.types.net; namespace ln.http { @@ -17,7 +18,7 @@ namespace ln.http private int blen; private int bptr; - public IPEndPoint RemoteEndpoint { get; private set; } + public Endpoint RemoteEndpoint { get; private set; } public String Method { get; private set; } public String URL { get; private set; } @@ -31,7 +32,7 @@ namespace ln.http { Stream = stream; } - public HttpReader(Stream stream,IPEndPoint remoteEndpoint) + public HttpReader(Stream stream,Endpoint remoteEndpoint) { Stream = stream; RemoteEndpoint = remoteEndpoint; @@ -84,7 +85,11 @@ namespace ln.http bptr = 0; do { - blen += Stream.Read(buffer, blen, buffer.Length - blen); + int rlen = Stream.Read(buffer, blen, buffer.Length - blen); + if (rlen == 0) + throw new IOException(); + + blen += rlen; while (bptr <= (blen - 4)) { if ( diff --git a/HttpRequest.cs b/HttpRequest.cs index 90f6a41..44adf6a 100644 --- a/HttpRequest.cs +++ b/HttpRequest.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Sockets; using ln.http.exceptions; using ln.http.resources.session; +using ln.types.net; namespace ln.http { @@ -15,10 +16,10 @@ namespace ln.http Dictionary requestHeaders; Dictionary requestCookies; - public HTTPServer HTTPServer { get; } + public HTTPServer HTTPServer { get; } - public IPEndPoint RemoteEndpoint { get; private set; } - public IPEndPoint LocalEndpoint { get; private set; } + public Endpoint RemoteEndpoint { get; private set; } + public Endpoint LocalEndpoint { get; private set; } public Uri BaseURI { get; set; } public Uri URI { get; private set; } @@ -54,7 +55,7 @@ namespace ln.http byte[] requestBody; Stream connectionStream; - public HttpRequest(HTTPServer httpServer, HttpReader httpReader, IPEndPoint localEndpoint) + public HttpRequest(HTTPServer httpServer, HttpReader httpReader, Endpoint localEndpoint) { HTTPServer = httpServer; connectionStream = httpReader.Stream; diff --git a/HttpResponse.cs b/HttpResponse.cs index b0f2c6b..afc0aa1 100644 --- a/HttpResponse.cs +++ b/HttpResponse.cs @@ -49,11 +49,12 @@ namespace ln.http SetHeader("content-type", contentType); } - - public String GetHeader(string name) + public String GetHeader(string name) => GetHeader(name, null); + public String GetHeader(string name, string defValue) { - return String.Join(",", headers[name.ToUpper()]); + return headers.ContainsKey(name) ? String.Join(",", headers[name.ToUpper()]) : defValue; } + public String[] GetHeaderValues(string name) { return headers[name.ToUpper()].ToArray(); @@ -84,6 +85,10 @@ namespace ln.http headers.Remove(name.ToUpper()); } + public bool ContainsHeader(string headerName) + { + return headers.ContainsKey(headerName.ToUpper()); + } public void AddCookie(string name,string value) { diff --git a/cert/CertContainer.cs b/cert/CertContainer.cs new file mode 100644 index 0000000..4f05846 --- /dev/null +++ b/cert/CertContainer.cs @@ -0,0 +1,59 @@ +// /** +// * File: CertContainer.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Security.Cryptography.X509Certificates; +using System.Collections.Generic; +using System.IO; + +namespace ln.http.cert +{ + public class CertContainer + { + public string SearchPath { get; set; } + + Dictionary certificates = new Dictionary(); + + public CertContainer(){ } + public CertContainer(string searchPath) + { + SearchPath = searchPath; + } + + public void AddCertificate(string targetHost, X509Certificate certificate) => certificates[targetHost] = certificate; + + public virtual X509Certificate LookupCertificate(string targetHost) + { + String p = Path.Combine(SearchPath, String.Format("{0}.pem",targetHost)); + if (File.Exists(p)) + { + return X509Certificate.CreateFromCertFile(p); + } + return null; + } + + public X509Certificate SelectCertificate(object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers) + { + if (!certificates.ContainsKey(targetHost) && (SearchPath != null)) + { + X509Certificate certificate = LookupCertificate(targetHost); + if (certificate != null) + { + certificates[targetHost] = certificate; + } + else + { + return null; + } + } + return certificates[targetHost]; + } + + } +} diff --git a/connections/Connection.cs b/connections/Connection.cs new file mode 100644 index 0000000..93369fe --- /dev/null +++ b/connections/Connection.cs @@ -0,0 +1,80 @@ +// /** +// * File: Connection.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using ln.types; +using System.IO; +using ln.types.net; +using ln.logging; +using ln.http.listener; +namespace ln.http.connections +{ + public abstract class Connection + { + public Listener Listener { get; } + + public abstract IPv6 RemoteHost { get; } + public abstract int RemotePort { get; } + + public abstract Stream GetStream(); + + public Connection(Listener listener) + { + Listener = listener; + } + + public virtual HttpRequest ReadRequest(HTTPServer httpServer) + { + try + { + HttpReader httpReader = new HttpReader(GetStream()); + httpReader.Read(); + + return new HttpRequest(httpServer, httpReader, Listener.LocalEndpoint); + } catch (IOException) + { + return null; + } catch (Exception e) + { + Logging.Log(e); + return null; + } + } + + public virtual void SendResponse(HttpResponse response) => SendResponse(GetStream(), response); + + public static void SendResponse(Stream stream, HttpResponse response) + { + response.SetHeader("Content-Length", response.ContentStream.Length.ToString()); + + StreamWriter streamWriter = new StreamWriter(stream); + streamWriter.NewLine = "\r\n"; + + streamWriter.WriteLine("{0} {1} {2}", response.HttpRequest.Protocol, response.StatusCode, response.StatusMessage); + foreach (String headerName in response.GetHeaderNames()) + { + streamWriter.WriteLine("{0}: {1}", headerName, response.GetHeader(headerName)); + } + + foreach (HttpCookie httpCookie in response.Cookies) + { + streamWriter.WriteLine("Set-Cookie: {0}", httpCookie.ToString()); + } + + streamWriter.WriteLine(); + streamWriter.Flush(); + + response.ContentStream.CopyTo(stream); + response.ContentStream.Close(); + response.ContentStream.Dispose(); + + stream.Flush(); + } + } +} diff --git a/connections/HttpConnection.cs b/connections/HttpConnection.cs new file mode 100644 index 0000000..1d7d421 --- /dev/null +++ b/connections/HttpConnection.cs @@ -0,0 +1,37 @@ +// /** +// * File: HttpConnection.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using ln.types; +using ln.types.net; +using System.Net.Sockets; +using ln.http.listener; + +namespace ln.http.connections +{ + public class HttpConnection : Connection + { + public TcpClient TcpClient { get; } + public Endpoint RemoteEndpoint { get; } + + public HttpConnection(Listener listener, TcpClient tcpClient) + :base(listener) + { + TcpClient = tcpClient; + RemoteEndpoint = new Endpoint(TcpClient.Client.RemoteEndPoint); + } + + public override IPv6 RemoteHost => RemoteEndpoint.Address; + public override int RemotePort => RemoteEndpoint.Port; + + public override Stream GetStream() => TcpClient.GetStream(); + + } +} diff --git a/connections/HttpsConnection.cs b/connections/HttpsConnection.cs new file mode 100644 index 0000000..9695de6 --- /dev/null +++ b/connections/HttpsConnection.cs @@ -0,0 +1,42 @@ +// /** +// * File: HttpsConnection.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using System.Security.AccessControl; +using ln.types; +using System.Net.Security; +using ln.http.listener; +using ln.types.net; + +namespace ln.http.connections +{ + public class HttpsConnection : Connection + { + Connection Connection { get; } + SslStream sslStream { get; } + + public override IPv6 RemoteHost => Connection.RemoteHost; + public override int RemotePort => Connection.RemotePort; + + public HttpsConnection(Listener listener,Connection connection,LocalCertificateSelectionCallback localCertificateSelectionCallback) + :base(listener) + { + Connection = connection; + sslStream = new SslStream(connection.GetStream(),false, null, localCertificateSelectionCallback); + } + + public override HttpRequest ReadRequest(HTTPServer server) + { + throw new NotImplementedException(); + } + + public override Stream GetStream() => sslStream; + } +} diff --git a/listener/HttpListener.cs b/listener/HttpListener.cs new file mode 100644 index 0000000..1482b94 --- /dev/null +++ b/listener/HttpListener.cs @@ -0,0 +1,55 @@ +// /** +// * File: HttpListener.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.Net.Sockets; +using ln.types; +using ln.types.net; +using ln.http.connections; + +namespace ln.http.listener +{ + public class HttpListener : Listener + { + + protected TcpListener tcpListener; + + public HttpListener(int port) :this(IPv6.ANY,port){} + public HttpListener(Endpoint endpoint) : this(endpoint.Address, endpoint.Port) {} + public HttpListener(IPv4 listen, int port) : this((IPv6)listen,port){} + public HttpListener(IPv6 listen, int port) + : base(listen, port) + { + } + + public override Connection Accept() => new HttpConnection(this,tcpListener.AcceptTcpClient()); + + public override bool IsOpen => tcpListener != null; + public override void Open() + { + tcpListener = new TcpListener(Listen, Port); + tcpListener.Start(); + } + public override void Close() + { + if (tcpListener != null) + { + tcpListener.Stop(); + tcpListener = null; + } + } + + public override void Dispose() + { + if (IsOpen) + Close(); + } + + } +} diff --git a/listener/HttpsListener.cs b/listener/HttpsListener.cs new file mode 100644 index 0000000..3bde884 --- /dev/null +++ b/listener/HttpsListener.cs @@ -0,0 +1,30 @@ +// /** +// * File: HttpsListener.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using ln.types; +using ln.types.net; +using ln.http.connections; +using ln.http.cert; +namespace ln.http.listener +{ + public class HttpsListener : HttpListener + { + public HttpsListener(int port) : this(IPv6.ANY, port) { } + public HttpsListener(IPv4 listen, int port) : this((IPv6)listen, port) { } + public HttpsListener(IPv6 listen, int port) : base(listen, port) { } + + public CertContainer CertContainer { get; set; } = new CertContainer(); + + public override Connection Accept() + { + return new HttpsConnection(this, base.Accept(),CertContainer.SelectCertificate); + } + } +} diff --git a/listener/Listener.cs b/listener/Listener.cs new file mode 100644 index 0000000..91745e1 --- /dev/null +++ b/listener/Listener.cs @@ -0,0 +1,44 @@ +// /** +// * File: Listener.cs +// * Author: haraldwolff +// * +// * This file and it's content is copyrighted by the Author and / or copyright holder. +// * Any use wihtout proper permission is illegal and may lead to legal actions. +// * +// * +// **/ +using System; +using System.IO; +using ln.http.connections; +using ln.types.net; +namespace ln.http.listener +{ + public abstract class Listener : IDisposable + { + public IPv6 Listen { get; } + public int Port { get; } + + public Endpoint LocalEndpoint => new Endpoint(Listen, Port); + + public abstract bool IsOpen { get; } + + protected Listener(IPv6 listen, int port) + { + Listen = listen; + Port = port; + } + + public virtual void AcceptMany(Action handler) + { + while (IsOpen) + handler(Accept()); + } + + public abstract void Open(); + public abstract void Close(); + + public abstract Connection Accept(); + + public abstract void Dispose(); + } +} diff --git a/ln.http.csproj b/ln.http.csproj index cfb4ac7..65a7cf8 100644 --- a/ln.http.csproj +++ b/ln.http.csproj @@ -29,6 +29,7 @@ + @@ -57,12 +58,22 @@ + + + + + + + + + + diff --git a/websocket/WebSocket.cs b/websocket/WebSocket.cs index 82be50e..703ee45 100644 --- a/websocket/WebSocket.cs +++ b/websocket/WebSocket.cs @@ -5,6 +5,7 @@ using ln.logging; using ln.http.exceptions; using System.Security.Cryptography; using System.Text; +using ln.types; namespace ln.http.websocket { @@ -32,7 +33,7 @@ namespace ln.http.websocket public delegate void WebSocketEventDelegate(WebSocket sender,WebSocketEventArgs e); - public class WebSocket + public abstract class WebSocket { public HTTPServer HTTPServer => HttpRequest.HTTPServer; public HttpRequest HttpRequest { get; } @@ -40,10 +41,6 @@ namespace ln.http.websocket public WebSocketState State { get; private set; } = WebSocketState.HANDSHAKE; - public event WebSocketEventDelegate WebSocketEvent; - - Thread receiverThread; - public WebSocket(HttpRequest httpRequest) { HttpRequest = httpRequest; @@ -72,22 +69,11 @@ namespace ln.http.websocket ); HTTPServerConnection.SendResponse(Stream, httpResponse); - HTTPServerConnection.Current.Value.AbortRequested += (connection) => Close(); - State = WebSocketState.OPEN; } - public bool IsAlive => ((receiverThread != null) && receiverThread.IsAlive); - - public void Start() - { - if ((receiverThread == null) || !receiverThread.IsAlive) - { - receiverThread = new Thread(() => Run()); - receiverThread.Start(); - } - } + public bool IsAlive => false; public void Close() { @@ -122,8 +108,10 @@ namespace ln.http.websocket switch (webSocketFrame.Opcode) { case WebSocketOpcode.TEXT: + Received(Encoding.UTF8.GetString(webSocketFrame.ApplicationData)); + break; case WebSocketOpcode.BINARY: - WebSocketEvent(this, new WebSocketEventArgs(webSocketFrame)); + Received(webSocketFrame.ApplicationData); break; case WebSocketOpcode.CLOSE: if (State == WebSocketState.OPEN) @@ -159,8 +147,17 @@ namespace ln.http.websocket } finally { } + } - receiverThread = null; + public virtual bool Received(string textMessage) + { + Logging.Log(LogLevel.WARNING, "WebSocket received unexpected text message:\n{0}", textMessage); + return false; + } + public virtual bool Received(byte[] binaryMessage) + { + Logging.Log(LogLevel.WARNING, "WebSocket received unexpected binary message:\n{0}",binaryMessage.ToHexString()); + return false; } public void Send(WebSocketFrame frame)