using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.IO; using System.Reflection; using System.Linq; using System.Text.RegularExpressions; namespace System.Net.ApiService { public class StateObject { // Client socket. public Socket workSocket = null; // Size of receive buffer. public const int BufferSize = 2048; // Receive buffer. public byte[] buffer = new byte[BufferSize]; // Received data string. public MemoryStream Content = new MemoryStream(); public StringBuilder RawHeader = new StringBuilder(); public RequestHeader Request; public ApiServer server; } public class ApiServer { public static ManualResetEvent allDone = new ManualResetEvent(false); protected ushort _port; protected HttpMethod _allowedMethods = HttpMethod.GET | HttpMethod.POST; private List _routes = new List(); protected string _serverName = "BlackPro_SimpleApi"; // TODO: wwwDirectory entsprechend einer Hosts.ini Datei anpassen, anhand der Request.Host wird ein entsprechender wwwDirectory / FavIcon / Robots.txt verwendet, der in der Hosts.ini hinterlegt wurde // Das soll Multiple V-Hosts in zukunft erlauben protected string? _wwwDirectory = "/www"; protected bool bUseWebDirectory = true; protected bool bUseCompression = false; protected bool bAllowEmptyResponse = false; protected Socket listener; protected bool bRunning; protected Thread th; protected string[] _allowedEncodingTypes = new string[] { "text/html", "application/javascript", "text/javascript", "application/json", "text/json", "text/css", "text/stylesheet", "text/xml", "application/xml" }; public HttpMethod AllowedMethods { get => this._allowedMethods; set => this._allowedMethods = value; } public RouteElements[] Routes { get { return this._routes.ToArray(); } } public bool Running { get { return this.bRunning; } } public bool AllowEmptyResponse { get { return this.bAllowEmptyResponse; } set { this.bAllowEmptyResponse = value; } } public bool Compression { get { return this.bUseCompression; } set { this.bUseCompression = value; } } public string ServerName { get { return this._serverName; } set { this._serverName = value; } } public bool UseWebDirectory { get { return this.bUseWebDirectory; } } public string WebDirectory { get { return this._wwwDirectory; } set { this._wwwDirectory = value; this.bUseWebDirectory = this._wwwDirectory != null && Directory.Exists(this._wwwDirectory) && this._wwwDirectory != ""; } } public ushort Port { get { return this._port; } set { this._port = value; if (this.bRunning) { this.Stop(); this.Start(); } } } public ApiServer() { } public ApiServer(ushort port) { this._port = port; } public ApiServer(string www_Directory, ushort port) { this._port = port; this._wwwDirectory = www_Directory; } public void Stop() { this.bRunning = false; System.Diagnostics.Debug.WriteLine("Stop"); try { allDone.Set(); this.th.Abort(); this.listener.Close(); this.listener.Dispose(); this.listener = null; } catch { } } public void Start() { if (this.bRunning == true) return; this.bRunning = true; this.th = new Thread(new ThreadStart(this.StartListen)); //this.th.ThreadState = new object[] { this.listener, this._port, this }; System.Diagnostics.Debug.WriteLine("Start"); this.th.Start(); } protected void StartListen() { // Establish the local endpoint for the socket. // The DNS name of the computer // running the listener is "host.contoso.com". IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName()); //IPAddress ipAddress = ipHostInfo.AddressList[0]; IPAddress ipAddress = new IPAddress(new byte[] { 127, 0, 0, 1 }); IPEndPoint localEndPoint = new IPEndPoint(ipAddress, this._port); // Create a TCP/IP socket. System.Diagnostics.Debug.WriteLine("Create listener"); //this.listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); this.listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // Bind the socket to the local endpoint and listen for incoming connections. try { System.Diagnostics.Debug.WriteLine("Bind listener"); this.listener.Bind(localEndPoint); System.Diagnostics.Debug.WriteLine("Listen on port: " + this._port); this.listener.Listen(100); while (this.bRunning) { // Set the event to nonsignaled state. System.Diagnostics.Debug.WriteLine("Reset"); allDone.Reset(); // Start an asynchronous socket to listen for connections. System.Diagnostics.Debug.WriteLine("Begin Accept"); this.listener.BeginAccept(new AsyncCallback(Async_AcceptCallback), new object[] { this.listener, this }); //Accept(this, this.listener); // Wait until a connection is made before continuing. System.Diagnostics.Debug.WriteLine("WaitOne"); allDone.WaitOne(); System.Diagnostics.Debug.WriteLine("Wait done"); } System.Diagnostics.Debug.WriteLine("Stop Listen"); } catch (Exception e) { System.Diagnostics.Debug.WriteLine(e); } } protected static void Accept(ApiServer server, Socket listener) { // Get the socket that handles the client request. Socket handler = listener.Accept(); System.Diagnostics.Debug.WriteLine("AcceptCallback"); allDone.Set(); // Create the state object. StateObject state = new StateObject(); state.workSocket = handler; state.server = server; System.Diagnostics.Debug.WriteLine("Begin Receive"); handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(Async_ReadRequestCallback), state); } protected static void Async_AcceptCallback(IAsyncResult ar) { // Signal the main thread to continue. System.Diagnostics.Debug.WriteLine("AcceptCallback"); allDone.Set(); // Get the socket that handles the client request. Socket listener = (Socket)((object[])ar.AsyncState)[0]; Socket handler = listener.EndAccept(ar); // Create the state object. StateObject state = new StateObject(); state.workSocket = handler; state.server = (ApiServer)((object[])ar.AsyncState)[1]; System.Diagnostics.Debug.WriteLine("Begin Receive"); handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(Async_ReadRequestCallback), state); } protected static void Async_ReadRequestCallback(IAsyncResult ar) { System.Diagnostics.Debug.WriteLine("ReadRequestCallback"); String content = String.Empty; // Retrieve the state object and the handler socket // from the asynchronous state object. StateObject state = (StateObject)ar.AsyncState; Socket handler = state.workSocket; // Read data from the client socket. int bytesRead = handler.EndReceive(ar); if (bytesRead > 0) { // There might be more data, so store the data received so far. string line = Encoding.ASCII.GetString(state.buffer, 0, bytesRead); if (state.RawHeader.Length == 0) { string[] xline = line.Split("\r\n\r\n", 2); state.RawHeader.Append(xline[0]); if (xline.Length > 0) { for (int i = 20; i < bytesRead; i++) if (state.buffer[i] == '\r' && state.buffer[i + 1] == '\n' && state.buffer[i + 2] == '\r' && state.buffer[i + 3] == '\n') { state.Content.Write(state.buffer,i+4,bytesRead-i-4); break; } } state.Request = new RequestHeader(state.RawHeader.ToString()); } else { state.Content.Write(state.buffer, 0, bytesRead); } Response resp = new Response(); RequestEventArgs req = new RequestEventArgs(); req.Response = resp; req.Request = state.Request; req.Socket = handler; req.SendAnyResponse = true; // Check if more data is incomming bool ContinueRead = false; HttpMethod requestsWithContent = HttpMethod.POST | HttpMethod.PUT | HttpMethod.PATCH; if ((state.Request.Method | requestsWithContent) == requestsWithContent && state.Request.ContentLength > 0) { ContinueRead = (state.Request.ContentLength > 0 && state.Request.ContentLength < state.Content.Length) || (state.Request.Boundary.Length > 0 && !line.EndsWith("--" + state.Request.Boundary + "--")); if (!ContinueRead && state.Request.Boundary.Length > 0) { // TODO: Resolver boundary here /* --boundary\r\n Content-Disposition: form-data; name="field1"\r\n \r\n Hier steht der Content, bis der nächste boundary beginnt\r\n --boundary\r\n Content-Disposition: form-data; name="field2"; filename="example.txt"\r\n \r\n Hier steht der Zweite Content, bis der finale boundary tag beginnt\r\n --boundary-- */ } } if (ContinueRead) { handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(Async_ReadRequestCallback), state); } else { // Check known files and check allowed HTTP-Method if ((state.Request.Path == "/ping" || state.Request.Path == "ping") && state.Request.Method == HttpMethod.GET) { resp.FromString(200, "pong"); } else if (state.Request.Path == "/favicon.ico" && state.Request.Method == HttpMethod.GET) { resp.FromFile("favicon.ico"); } else if (state.Request.Path == "/robots.txt" && state.Request.Method == HttpMethod.GET) { resp.FromFile("robots.txt"); } else { if(state.Request.ContentLength>0 && state.Request.ContentLength<2048) { state.Request.SetPostData(state.Content.ToString()); } if ((state.Request.Method | state.server._allowedMethods) != state.server._allowedMethods) { resp.Error(Response.eErrorCode.MethodNotAllowed); } else { state.server.RequestAttr(req); state.server.OnRequest(req); } } // Generate response header List response_header = new List(); if (state.server.bAllowEmptyResponse && req.Response.Length == 0) { SetDefaultResponse(req.Response); } response_header.Add("HTTP/1.1 " + req.Response.Code + " " + req.Response.CodeName); response_header.Add("Server: " + state.server.ServerName); response_header.Add("Date: " + DateTime.Now.ToString("r")); bool bUseBRCompression = false; bool bUseGZCompression = false; bool bUseDeflateCompression = false; Span compressed_content = new Span(); int bytesWritten; if (req.Response.Code >= 200 && req.Response.Code < 300 && resp.FilePath == "" && state.server.bUseCompression) { if (state.server._allowedEncodingTypes.Contains(req.Response.ContentType)) { if ((state.Request.AcceptEncoding & RequestHeader.eEncoder.br) == state.Request.AcceptEncoding) { if (System.IO.Compression.BrotliEncoder.TryCompress(resp.Content, compressed_content, out bytesWritten)) { bUseBRCompression = true; } } else if ((state.Request.AcceptEncoding & RequestHeader.eEncoder.gzip) == state.Request.AcceptEncoding) { using (MemoryStream mem = new MemoryStream()) { using (System.IO.Compression.GZipStream gzip = new IO.Compression.GZipStream(mem, IO.Compression.CompressionLevel.Optimal)) { gzip.Write(resp.Content, 0, resp.Content.Length); } compressed_content = mem.ToArray(); } bUseGZCompression = true; } else if ((state.Request.AcceptEncoding & RequestHeader.eEncoder.deflate) == state.Request.AcceptEncoding) { using (MemoryStream mem = new MemoryStream()) { using (System.IO.Compression.DeflateStream defl = new IO.Compression.DeflateStream(mem, IO.Compression.CompressionLevel.Optimal)) { defl.Write(resp.Content, 0, resp.Content.Length); } compressed_content = mem.ToArray(); } bUseDeflateCompression = true; } } } if (req.Response.Code >= 300 && req.Response.Code < 400 && req.Response.RedirectLocation != "") { response_header.Add("Location: " + req.Response.RedirectLocation); } else if (req.Response.Code >= 500) { // Nothing to do on 500 errors } else { if (bUseBRCompression || bUseGZCompression || bUseDeflateCompression) response_header.Add("Content-Length: " + compressed_content.Length); else response_header.Add("Content-Length: " + req.Response.Length); if (req.Response.ContentType == "") { response_header.Add("Content-Type: " + req.Response.ContentType); } response_header.Add("Accept-Ranges: bytes"); response_header.Add("Age: 0"); response_header.Add("Accept-Ranges: bytes"); if (resp.CacheControl != null && resp.CacheControl.IsSet) { response_header.Add("Cache-Control: " + resp.CacheControl); } if (bUseBRCompression) response_header.Add("Content-Encoding: bz"); else if (bUseGZCompression) response_header.Add("Content-Encoding: gzip"); else if (bUseDeflateCompression) response_header.Add("Content-Encoding: deflate"); response_header.Add("Connection: " + (resp.Connection == Response.eConnection.KeepAlive ? "keep-alive" : "Close")); } string response_header_string = ""; foreach (string s in response_header) response_header_string += s + "\r\n"; response_header_string += "\r\n"; Send(handler, response_header_string, resp.Content); } } } protected static void SetDefaultResponse(Response res) { switch (res.ContentType.ToLower().Trim()) { case "": case "text/html": res.FromString("", "text/html"); break; case "text/stylesheet": case "text/css": res.FromString(""); break; case "text/javascript": case "application/javascript": case "text/x-javascript": case "application/x-javascript": res.FromString(""); break; } } private static void Send(Socket handler, String data, byte[] Content) { // Convert the string data to byte data using ASCII encoding. byte[] byteData = Encoding.ASCII.GetBytes(data); // Begin sending the data to the remote device. handler.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendResponseHeaderCallback), new object[] { handler, Content }); } private static void SendResponseHeaderCallback(IAsyncResult ar) { try { // Retrieve the socket from the state object. Socket handler = (Socket)((object[])ar.AsyncState)[0]; byte[] Content = (byte[])((object[])ar.AsyncState)[1]; // Complete sending the data to the remote device. int bytesSent = handler.EndSend(ar); handler.BeginSend(Content, 0, Content.Length, 0, new AsyncCallback(SendResponseContentCallback), handler); } catch (Exception e) { System.Diagnostics.Debug.WriteLine(e); } } private static void SendResponseContentCallback(IAsyncResult ar) { try { // Retrieve the socket from the state object. Socket handler = (Socket)ar.AsyncState; // Complete sending the data to the remote device. int bytesSent = handler.EndSend(ar); //Console.WriteLine("Sent {0} bytes to client.", bytesSent); handler.Shutdown(SocketShutdown.Both); handler.Close(); } catch (Exception e) { System.Diagnostics.Debug.WriteLine(e); } } public void RegisterClass(object[] instances) { foreach (object o in instances) this.RegisterClass(o); } public void RegisterClass(Type[] t) { foreach (Type o in t) this.RegisterClass(o); } public void RegisterClass(object instance) { Type t = instance.GetType(); MethodInfo[] methodInfos = t.GetMethods(BindingFlags.Public | BindingFlags.Static); Route r = t.GetCustomAttribute(); string baseRoute = ""; if (r != null) { baseRoute = r.Path; } foreach (MethodInfo mi in methodInfos) { Route[] ris = mi.GetCustomAttributes().ToArray(); foreach (Route ri in ris) { this._routes.Add(new RouteElements(instance, mi, ri, baseRoute)); } } } public void RegisterClass(Type t) { Route r = t.GetCustomAttribute(); string baseRoute = ""; if (r != null) { baseRoute = r.Path; } MethodInfo[] methodInfos = t.GetMethods(BindingFlags.Public | BindingFlags.Static); foreach (MethodInfo mi in methodInfos) { Route[] ris = mi.GetCustomAttributes().ToArray(); foreach (Route ri in ris) { this._routes.Add(new RouteElements(t, mi, ri, baseRoute)); } } } protected void RequestAttr(RequestEventArgs e) { RequestHeader rh; foreach (RouteElements re in this._routes) { rh = e.Request; bool bIsMethod = (rh.Method | re.Method) == re.Method; if (rh.Path != null && re.Pattern.IsMatch(rh.Path) && bIsMethod) { ParameterInfo[] pis = re.Function.GetParameters(); if (pis.Length == 2 && pis[0].ParameterType == typeof(RequestHeader) && pis[1].ParameterType == typeof(Response)) { re.Function.Invoke(re.Instance, new object[] { e.Request, e.Response }); } else if (pis.Length == 1 && pis[0].ParameterType == typeof(RequestHeader)) { if (re.Function.ReturnType == typeof(Response)) { Response r = (Response)re.Function.Invoke(re.Instance, new object[] { rh }); if (r != null) { e.Response = r; } } else if (re.Function.ReturnType == typeof(string)) { string r = (string)re.Function.Invoke(re.Instance, new object[] { rh }); e.Response.FromString(200, r); } else if (re.Function.ReturnType == typeof(bool)) { bool r = (bool)re.Function.Invoke(re.Instance, new object[] { rh }); if (r) e.Response.FromString(200, "{\"status\":\"ok\",\"code\":200,\"value\":1}", "application/json"); else e.Response.FromString(200, "{\"status\":\"failed\",\"code\":200,\"value\":0}", "application/json"); } else if (re.Function.ReturnType == typeof(object)) { object r = (object)re.Function.Invoke(re.Instance, new object[] { rh }); e.Response.FromObject(200, r); } } else if (pis.Length == 1 && pis[0].ParameterType == typeof(Response)) { re.Function.Invoke(re.Instance, new object[] { e.Response }); //RegEx } else if (pis.Length == 3 && pis[0].ParameterType == typeof(RequestHeader) && pis[1].ParameterType == typeof(Response) && pis[2].ParameterType == typeof(Match)) { re.Function.Invoke(re.Instance, new object[] { e.Request, e.Response, re.Pattern.Match(rh.Path) }); } else if (pis.Length == 2 && pis[0].ParameterType == typeof(RequestHeader) && pis[1].ParameterType == typeof(Match)) { if (re.Function.ReturnType == typeof(Response)) { Response r = (Response)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path) }); if (r != null) { e.Response = r; } } else if (re.Function.ReturnType == typeof(string)) { string r = (string)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path) }); e.Response.FromString(200, r); } else if (re.Function.ReturnType == typeof(bool)) { bool r = (bool)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path) }); if (r) e.Response.FromString(200, "{\"status\":\"ok\",\"code\":200,\"value\":1}", "application/json"); else e.Response.FromString(200, "{\"status\":\"failed\",\"code\":200,\"value\":0}", "application/json"); } else if (re.Function.ReturnType == typeof(object)) { object r = (object)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path) }); e.Response.FromObject(200, r); } } else if (pis.Length == 2 && pis[0].ParameterType == typeof(Response) && pis[1].ParameterType == typeof(Match)) { re.Function.Invoke(re.Instance, new object[] { e.Response, re.Pattern.Match(rh.Path) }); } else if (pis.Length == 3 && pis[0].ParameterType == typeof(RequestHeader) && pis[1].ParameterType == typeof(Response) && pis[2].ParameterType == typeof(GroupCollection)) { re.Function.Invoke(re.Instance, new object[] { e.Request, e.Response, re.Pattern.Match(rh.Path).Groups }); } else if (pis.Length == 2 && pis[0].ParameterType == typeof(RequestHeader) && pis[1].ParameterType == typeof(GroupCollection)) { if (re.Function.ReturnType == typeof(Response)) { Response r = (Response)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path).Groups }); if (r != null) { e.Response = r; } } else if (re.Function.ReturnType == typeof(string)) { string r = (string)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path).Groups }); e.Response.FromString(200, r); } else if (re.Function.ReturnType == typeof(bool)) { bool r = (bool)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path).Groups }); if (r) e.Response.FromString(200, "{\"status\":\"ok\",\"code\":200,\"value\":1}", "application/json"); else e.Response.FromString(200, "{\"status\":\"failed\",\"code\":200,\"value\":0}", "application/json"); } else if (re.Function.ReturnType == typeof(object)) { object r = (object)re.Function.Invoke(re.Instance, new object[] { rh, re.Pattern.Match(rh.Path).Groups }); e.Response.FromObject(200, r); } } else if (pis.Length == 2 && pis[0].ParameterType == typeof(Response) && pis[1].ParameterType == typeof(GroupCollection)) { re.Function.Invoke(re.Instance, new object[] { e.Response, re.Pattern.Match(rh.Path).Groups }); } } } } public virtual void OnRequest(RequestEventArgs e) { EventHandler handler = Request; if (handler != null) { handler(this, e); } } public event EventHandler Request; } }