using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using System.Collections; using System.Collections.Generic; using System.Reflection; using System.Linq; using System.Text.RegularExpressions; namespace System.Net.ApiService { /* * Useage: * - Create an instance with Port * - If you want to use EventHandler, api.Request += Api_Request; * private void Api_Request(object sender, RequestEventArgs e) * - If you want to use Annotations, create a function and add [Route("/index.html") above it * Multiple Routes are allowed * private void example1(RequestHeader rh, Response r) * private void example1(RequestHeader rh, Response r, Match m) * private void example1(RequestHeader rh, Response r, GroupCollection m) * private string example1(RequestHeader rh) * private object example1(RequestHeader rh) * private bool example1(RequestHeader rh) * private string example1(RequestHeader rh, Match m) * private object example1(RequestHeader rh, Match m) * private bool example1(RequestHeader rh, Match m) * private string example1(RequestHeader rh, GroupCollection m) * private object example1(RequestHeader rh, GroupCollection m) * private bool example1(RequestHeader rh, GroupCollection m) * [...] * - Return Types: * void, string, bool, object * - Parameters * RequestHeader, Response?, Match?/GroupCollection? * - Using RegEx, "." and "/" only match inside a () {} and [], so "/index.html" will link to a file instead of "(/index.html)" that would match "indexAhtml", too */ public class HttpApiServer { protected TcpListener myListener; protected ushort _port; protected bool bRunning = false; protected Thread th; private List _routes = new List(); protected HttpMethod _allowedMethods = HttpMethod.GET | HttpMethod.POST; 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 string Path_FavIcon = "/favicon.ico"; protected string Path_RobotsTXT = "/robots.txt"; private string[] _allowedEncodingTypes = new string[] { "text/html", "application/javascript", "text/javascript", "application/json", "text/json", "text/css", "text/stylesheet", "text/xml", "application/xml" }; public RouteElements[] Routes { get { return this._routes.ToArray(); } } public bool Running { get { return this.bRunning; } } public HttpApiServer(ushort port) { this.Port = port; bUseWebDirectory = Directory.Exists(this._wwwDirectory); } public HttpApiServer(string www_directory, ushort port) { this.Port = port; this.WebDirectory = www_directory; } public bool AllowEmptyResponse { get { return this.bAllowEmptyResponse; } set { this.bAllowEmptyResponse = value; } } public HttpMethod AllowedMethods { get { return this._allowedMethods; } set { this._allowedMethods = 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(); myListener = new TcpListener(this._port); this.Start(); } else { myListener = new TcpListener(this._port); } //myListener.Start(); } } public bool Pending { get => this.myListener.Pending(); } public void Start() { this.bRunning = true; if (myListener==null) { myListener = new TcpListener(this._port); } myListener.Start(); this.th = new Thread(new ThreadStart(this.StartListen)); this.th.Start(); } public void Stop() { try { myListener.Server.Close(); myListener.Stop(); myListener = null; } catch { } try { this.th.Abort(); } catch { } this.bRunning = false; } /*public void SendHeader(string sHttpVersion, string sMIMEHeader, int iTotBytes, string sStatusCode, ref Socket mySocket) { String sBuffer = ""; // if Mime type is not provided set default to text/html if (sMIMEHeader.Length == 0) { sMIMEHeader = "text/html";// Default Mime Type is text/html } sBuffer = sBuffer + sHttpVersion + sStatusCode + "\r\n"; sBuffer = sBuffer + "Server: BPApiServer\r\n"; sBuffer = sBuffer + "Content-Type: " + sMIMEHeader + "\r\n"; sBuffer = sBuffer + "Accept-Ranges: bytes\r\n"; sBuffer = sBuffer + "Content-Length: " + iTotBytes + "\r\n\r\n"; Byte[] bSendData = Encoding.ASCII.GetBytes(sBuffer); SendToBrowser(bSendData, ref mySocket); }*/ public bool SendToBrowser(String sData, ref Socket mySocket) { return SendToBrowser(Encoding.ASCII.GetBytes(sData), ref mySocket); } public bool SendToBrowser(Byte[] bSendData, ref Socket mySocket) { int numBytes = 0; try { if (mySocket.Connected) { if ((numBytes = mySocket.Send(bSendData, bSendData.Length, 0)) > 0) return true; } } catch (Exception e) { } return false; } protected void StartListen() { while (true) { //Accept a new connection Socket mySocket; try { mySocket = myListener.AcceptSocket(); } catch { break; } DateTime startTime = DateTime.Now; TimeSpan timeout = new TimeSpan(0, 0, 10); if (mySocket.Connected) { Byte[] bReceive = new Byte[4096]; int i = mySocket.Receive(bReceive, 0, mySocket.Available, SocketFlags.Peek); string sBuffer = Encoding.ASCII.GetString(bReceive, 0, i); string sBuffer2 = ""; foreach(byte b in bReceive) { if (b > 0) sBuffer2 += b; } if(sBuffer2.Length==0) { i = mySocket.Receive(bReceive, 0, mySocket.Available, SocketFlags.Peek); sBuffer = Encoding.ASCII.GetString(bReceive,0,i); foreach (byte b in bReceive) { if (b > 0) sBuffer2 += b; } } System.Diagnostics.Debug.WriteLine("- Received[" + sBuffer2.Length + "]: '" + sBuffer2.Trim() + "'"); RequestHeader header = new RequestHeader(sBuffer); Response resp = new Response(); RequestEventArgs req = new RequestEventArgs(); req.Response = resp; req.Request = header; req.Socket = mySocket; req.SendAnyResponse = true; if (header.Path == "/ping" || header.Path=="ping") { resp.FromString(200, "pong"); } else if (req.Request.Path == "/favicon.ico") { resp.FromFile("favicon.ico"); } else if (req.Request.Path == "/robots.txt") { resp.FromFile("robots.txt"); } else { if ((header.Method | this._allowedMethods) != this._allowedMethods) { resp.Error(Response.eErrorCode.MethodNotAllowed); } else { RequestAttr(req); OnRequest(req); } } if (req.Response == null) { req.Response = new Response(); req.Response.Error(Response.eErrorCode.InternalServerError); } if (req.Response == null) { req.Request = header; req.Response.Error(Response.eErrorCode.InternalServerError); } if (req.Response.Code < 400) { System.Diagnostics.Debug.WriteLine("Request: " + header.Path); if (!req.Response.IsSet) { System.Diagnostics.Debug.WriteLine("No response was set"); if (this.bUseWebDirectory) { System.Diagnostics.Debug.WriteLine("Use www-Directory"); System.Diagnostics.Debug.WriteLine("Dir: " + this._wwwDirectory); System.Diagnostics.Debug.WriteLine("File: " + req.Request.Path); FileInfo fi = new FileInfo(this._wwwDirectory + req.Request.Path); System.Diagnostics.Debug.WriteLine("Full: " + fi.FullName); if (fi.Exists) { req.Response.FromFile(fi.FullName); } } } System.Diagnostics.Debug.WriteLine("Done"); } if (req.SendAnyResponse) { List response_header = new List(); if(this.bAllowEmptyResponse && req.Response.Length==0) { SetDefaultResponse(req.Response); } response_header.Add("HTTP/1.1 " + req.Response.Code + " " + req.Response.CodeName); response_header.Add("Server: " + this._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 == "" && this.bUseCompression) { if (this._allowedEncodingTypes.Contains(req.Response.ContentType)) { if ((header.AcceptEncoding & RequestHeader.eEncoder.br) == header.AcceptEncoding) { if (System.IO.Compression.BrotliEncoder.TryCompress(resp.Content, compressed_content, out bytesWritten)) { bUseBRCompression = true; } } else if ((header.AcceptEncoding & RequestHeader.eEncoder.gzip) == header.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 ((header.AcceptEncoding & RequestHeader.eEncoder.deflate) == header.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"; System.Diagnostics.Debug.WriteLine(response_header_string); this.SendToBrowser(Encoding.ASCII.GetBytes(response_header_string), ref mySocket); if (req.Response.FilePath != "" && req.Response.Code == 200) { System.IO.FileStream fs = new FileStream(req.Response.FilePath, FileMode.Open); BinaryReader br = new BinaryReader(fs); while (fs.Position < fs.Length) { byte[] buffer = br.ReadBytes(1000); this.SendToBrowser(buffer, ref mySocket); } fs.Close(); } else { if (bUseBRCompression || bUseGZCompression || bUseDeflateCompression) { System.Diagnostics.Debug.WriteLine("Send compressed: " + compressed_content.Length); this.SendToBrowser(compressed_content.ToArray(), ref mySocket); } else { if (req.Response.Connection != null && req.Response.Length>0) { System.Diagnostics.Debug.WriteLine("Send uncompressed: " + req.Response.Content.Length); if (this.SendToBrowser(req.Response.Content, ref mySocket)) { System.Diagnostics.Debug.WriteLine("Success"); } else { System.Diagnostics.Debug.WriteLine("Failed"); } } } } } mySocket.Close(); } } } protected 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; } } 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 }); } } } } protected virtual void OnRequest(RequestEventArgs e) { EventHandler handler = Request; if (handler != null) { handler(this, e); } } public event EventHandler Request; } }