From 33ee2d4a47af266f3e539bb783b7e855338c651c Mon Sep 17 00:00:00 2001 From: "Valeriano A.R" Date: Sun, 28 Apr 2019 17:21:01 +0200 Subject: [PATCH] HttpServer, HttpProcessor and IHttpHandler. --- .gitignore | 7 + VAR.HttpServer.MiniServerTest/Program.cs | 39 +++ .../Properties/AssemblyInfo.cs | 15 + .../VAR.HttpServer.MiniServerTest.csproj | 56 ++++ VAR.HttpServer.sln | 31 +++ VAR.HttpServer/HttpProcessor.cs | 263 ++++++++++++++++++ VAR.HttpServer/HttpServer.cs | 110 ++++++++ VAR.HttpServer/HttpUtility.cs | 127 +++++++++ VAR.HttpServer/IHttpHandler.cs | 7 + VAR.HttpServer/Properties/AssemblyInfo.cs | 15 + VAR.HttpServer/VAR.HttpServer.csproj | 51 ++++ 11 files changed, 721 insertions(+) create mode 100644 .gitignore create mode 100644 VAR.HttpServer.MiniServerTest/Program.cs create mode 100644 VAR.HttpServer.MiniServerTest/Properties/AssemblyInfo.cs create mode 100644 VAR.HttpServer.MiniServerTest/VAR.HttpServer.MiniServerTest.csproj create mode 100644 VAR.HttpServer.sln create mode 100644 VAR.HttpServer/HttpProcessor.cs create mode 100644 VAR.HttpServer/HttpServer.cs create mode 100644 VAR.HttpServer/HttpUtility.cs create mode 100644 VAR.HttpServer/IHttpHandler.cs create mode 100644 VAR.HttpServer/Properties/AssemblyInfo.cs create mode 100644 VAR.HttpServer/VAR.HttpServer.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..beb198c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.exe +*.pdb +*/bin/* +*/obj/* +*.csproj.user +*.suo +.vs diff --git a/VAR.HttpServer.MiniServerTest/Program.cs b/VAR.HttpServer.MiniServerTest/Program.cs new file mode 100644 index 0000000..71357c4 --- /dev/null +++ b/VAR.HttpServer.MiniServerTest/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Diagnostics; + +namespace VAR.HttpServer.MiniServerTest +{ + internal class Program + { + private static void Main(string[] args) + { + HttpServer httpServer = new HttpServer + { + Port = 3000, + Handler = new HelloWorldHttpHandler(), + LogDegugMessage = (msg) => Console.WriteLine("DEBUG: {0}", msg), + LogException = (ex) => + { + Console.WriteLine("!!!!! Exception !!!!"); + Console.WriteLine("Message: {0}", ex.Message); + Console.WriteLine("StackTrace: {0}", ex.StackTrace); + } + }; + Console.Title = string.Format("MiniHTTPServer@{0}", httpServer.Port); + Console.WriteLine("HTTP Server started on {0} port", httpServer.Port); + httpServer.Start(); + + Process proc = Process.Start(string.Format("http://localhost:{0}", httpServer.Port)); + } + + public class HelloWorldHttpHandler : IHttpHandler + { + public void HandleRequest(HttpProcessor p) + { + Console.WriteLine("Responding to {0}", p.Socket.Client.RemoteEndPoint); + p.ResponseSuccess(); + p.OutputStream.WriteLine("Hello World!"); + } + } + } +} diff --git a/VAR.HttpServer.MiniServerTest/Properties/AssemblyInfo.cs b/VAR.HttpServer.MiniServerTest/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..eb39b13 --- /dev/null +++ b/VAR.HttpServer.MiniServerTest/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("VAR.HttpServer.MiniServerTest")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("VAR")] +[assembly: AssemblyProduct("VAR.HttpServer.MiniServerTest")] +[assembly: AssemblyCopyright("Copyright © VAR 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: Guid("6bd47b75-3dc2-4559-9311-e3d8117d4a89")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/VAR.HttpServer.MiniServerTest/VAR.HttpServer.MiniServerTest.csproj b/VAR.HttpServer.MiniServerTest/VAR.HttpServer.MiniServerTest.csproj new file mode 100644 index 0000000..a1b331b --- /dev/null +++ b/VAR.HttpServer.MiniServerTest/VAR.HttpServer.MiniServerTest.csproj @@ -0,0 +1,56 @@ + + + + + Debug + AnyCPU + {6BD47B75-3DC2-4559-9311-E3D8117D4A89} + Exe + VAR.HttpServer.MiniServerTest + VAR.HttpServer.MiniServerTest + v4.6.1 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + {e865ba90-11f7-46d4-b718-a536b0b76c35} + VAR.HttpServer + + + + \ No newline at end of file diff --git a/VAR.HttpServer.sln b/VAR.HttpServer.sln new file mode 100644 index 0000000..adfc365 --- /dev/null +++ b/VAR.HttpServer.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.572 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VAR.HttpServer", "VAR.HttpServer\VAR.HttpServer.csproj", "{E865BA90-11F7-46D4-B718-A536B0B76C35}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VAR.HttpServer.MiniServerTest", "VAR.HttpServer.MiniServerTest\VAR.HttpServer.MiniServerTest.csproj", "{6BD47B75-3DC2-4559-9311-E3D8117D4A89}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E865BA90-11F7-46D4-B718-A536B0B76C35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E865BA90-11F7-46D4-B718-A536B0B76C35}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E865BA90-11F7-46D4-B718-A536B0B76C35}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E865BA90-11F7-46D4-B718-A536B0B76C35}.Release|Any CPU.Build.0 = Release|Any CPU + {6BD47B75-3DC2-4559-9311-E3D8117D4A89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BD47B75-3DC2-4559-9311-E3D8117D4A89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BD47B75-3DC2-4559-9311-E3D8117D4A89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BD47B75-3DC2-4559-9311-E3D8117D4A89}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0CFDBA5E-334F-41CB-8AA0-EEF9191EDD64} + EndGlobalSection +EndGlobal diff --git a/VAR.HttpServer/HttpProcessor.cs b/VAR.HttpServer/HttpProcessor.cs new file mode 100644 index 0000000..ecedbde --- /dev/null +++ b/VAR.HttpServer/HttpProcessor.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; +using System.Threading; + +namespace VAR.HttpServer +{ + public class HttpProcessor + { + private static readonly int MaxPostSize = 10 * 1024 * 1024; // 10MB + + private TcpClient _socket; + public TcpClient Socket { get { return _socket; } } + + private IHttpHandler _handler; + public IHttpHandler Handler { get { return _handler; } } + + private readonly Action _logDebugMessage = null; + + private readonly Action _logException = null; + + private Stream _inputStream; + + private StreamWriter _outputStream; + public StreamWriter OutputStream { get { return _outputStream; } } + + private string _httpRequest; + + private string _httpMethod; + public string HttpMethod { get { return _httpMethod; } } + + private string _httpResource; + public string HttpResource { get { return _httpResource; } } + + private string _httpResourceLowercase; + public string HttpResourceLowercase { get { return _httpResourceLowercase; } } + + private string _httpQuery; + public string HttpQuery { get { return _httpQuery; } } + + private string _httpProtocolVersionString; + + private Dictionary _httpHeaders = new Dictionary(); + public Dictionary HttpHeader { get { return _httpHeaders; } } + + private string _postString; + public string PostString { get { return _postString; } } + + private Dictionary _httpParams = new Dictionary(); + public Dictionary HttpParams { get { return _httpParams; } } + + public bool IsPostback { get { return _httpMethod.ToUpper().Equals("POST"); } } + + public HttpProcessor(TcpClient s, IHttpHandler handler, Action logDebugMessage, Action logException) + { + _socket = s; + _handler = handler; + _logDebugMessage = logDebugMessage; + _logException = logException; + } + + public void Process() + { + try + { + _inputStream = new BufferedStream(_socket.GetStream()); + _outputStream = new StreamWriter(new BufferedStream(_socket.GetStream())); + try + { + _httpParams.Clear(); + _httpHeaders.Clear(); + ParseRequest(); + ReadHeaders(); + ReadPostData(); + if (_handler != null) + { + _handler.HandleRequest(this); + } + else + { + ResponseServerError(); + } + } + catch (Exception) + { + ResponseServerError(); + } + _outputStream.Flush(); + _inputStream = null; + _outputStream = null; + _socket.Close(); + } + catch (Exception ex) + { + _logException?.Invoke(ex); + } + } + + private string StreamReadLine(Stream inputStream) + { + int next_char; + string data = string.Empty; + while (true) + { + next_char = inputStream.ReadByte(); + if (next_char == '\n') { break; } + if (next_char == '\r') { continue; } + if (next_char == -1) { Thread.Sleep(1); continue; }; + data += Convert.ToChar(next_char); + } + return data; + } + + private void ParseRequest() + { + string request = StreamReadLine(_inputStream); + string[] tokens = request.Split(' '); + if (tokens.Length != 3) + { + throw new Exception("invalid HTTP request line"); + } + _httpRequest = request; + _httpMethod = tokens[0].ToUpper(); + _httpResource = tokens[1]; + _httpQuery = string.Empty; + _httpProtocolVersionString = tokens[2]; + + if (_httpResource.Contains("?")) + { + int idx = _httpResource.IndexOf('?'); + _httpQuery = _httpResource.Substring(idx + 1); + _httpResource = _httpResource.Substring(0, idx); + ReadParms(_httpQuery); + } + _httpResourceLowercase = _httpResource.ToLower(); + } + + private void ReadHeaders() + { + string line; + while ((line = StreamReadLine(_inputStream)) != null) + { + if (string.IsNullOrEmpty(line)) + { + return; + } + + int separator = line.IndexOf(':'); + if (separator == -1) + { + throw new Exception("invalid http header line: " + line); + } + string name = line.Substring(0, separator); + int pos = separator + 1; + while ((pos < line.Length) && (line[pos] == ' ')) + { + pos++; + } + + string value = line.Substring(pos, line.Length - pos); + _httpHeaders[name] = value; + } + } + + private const int BufferSize = 4096; + + private void ReadPostData() + { + int content_len = 0; + MemoryStream ms = new MemoryStream(); + if (this._httpHeaders.ContainsKey("Content-Length")) + { + content_len = Convert.ToInt32(this._httpHeaders["Content-Length"]); + if (content_len > MaxPostSize) + { + throw new Exception( + string.Format("POST Content-Length({0}) too big for this simple server", + content_len)); + } + byte[] buf = new byte[BufferSize]; + int to_read = content_len; + while (to_read > 0) + { + int numread = this._inputStream.Read(buf, 0, Math.Min(BufferSize, to_read)); + if (numread == 0) + { + if (to_read == 0) + { + break; + } + else + { + throw new Exception("client disconnected during post"); + } + } + to_read -= numread; + ms.Write(buf, 0, numread); + } + ms.Seek(0, SeekOrigin.Begin); + } + _postString = new StreamReader(ms).ReadToEnd(); + if (string.IsNullOrEmpty(_postString) == false) + { + ReadParms(_postString); + } + } + + private void ReadParms(string parms) + { + string[] parmPairs = parms.Split('&'); + foreach (string parmPair in parmPairs) + { + string[] tokens = parmPair.Split('='); + string key = HttpUtility.UrlDecode(tokens[0]); + string value = string.Empty; + if (tokens.Length > 1) + { + value = HttpUtility.UrlDecode(tokens[1]); + } + if (_httpParams.ContainsKey(key)) + { + _httpParams[key] = value; + } + else + { + _httpParams.Add(key, value); + } + } + } + + public void ResponseSuccess(string contentType = "text/html") + { + try + { + _outputStream.WriteLine("HTTP/1.0 200 OK"); + _outputStream.WriteLine(string.Format("Content-Type: {0}", contentType)); + _outputStream.WriteLine("Connection: close"); + _outputStream.WriteLine(""); + _outputStream.Flush(); + } + catch (Exception ex) + { + _logException?.Invoke(ex); + } + } + + public void ResponseServerError() + { + try + { + _outputStream.WriteLine("HTTP/1.0 500 Internal server error"); + _outputStream.WriteLine("Connection: close"); + _outputStream.WriteLine(""); + _outputStream.Flush(); + } + catch (Exception ex) + { + _logException?.Invoke(ex); + } + } + } +} diff --git a/VAR.HttpServer/HttpServer.cs b/VAR.HttpServer/HttpServer.cs new file mode 100644 index 0000000..79eed62 --- /dev/null +++ b/VAR.HttpServer/HttpServer.cs @@ -0,0 +1,110 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace VAR.HttpServer +{ + public class HttpServer + { + protected int _port = 8000; + public int Port + { + get { return _port; } + set + { + if (_isActive) { throw new Exception("HttpServer: Can't set port while active"); } + _port = value; + } + } + + private TcpListener _listener = null; + + private Thread _thread = null; + + private bool _isActive = false; + public bool IsActive { get { return _isActive; } } + + private IHttpHandler _handler = null; + public IHttpHandler Handler + { + get { return _handler; } + set { _handler = value; } + } + + private Action _logDebugMessage = null; + public Action LogDegugMessage { set { _logDebugMessage = value; } } + + private Action _logException = null; + public Action LogException { set { _logException = value; } } + + private void ListenConnections() + { + try + { + while (_isActive) + { + TcpClient client = _listener.AcceptTcpClient(); + HttpProcessor responseProcessor = new HttpProcessor(client, _handler, _logDebugMessage, _logException); + Thread responseThread = new Thread(new ThreadStart(responseProcessor.Process)); + responseThread.Start(); + } + _listener = null; + } + catch (Exception) + { + _isActive = false; + } + } + + public bool Start() + { + try + { + if (_isActive || _thread != null) + { + return false; + } + + _listener = new TcpListener(IPAddress.Any, _port) + { + ExclusiveAddressUse = false + }; + _listener.Start(); + _isActive = true; + _thread = new Thread(new ThreadStart(ListenConnections)); + _thread.Start(); + } + catch (Exception ex) + { + _logException?.Invoke(ex); + return false; + } + return true; + } + + public bool Stop() + { + try + { + if (_isActive == false || _thread == null) + { + return false; + } + + _isActive = false; + if (_listener != null) + { + _listener.Stop(); + } + _thread = null; + } + catch (Exception ex) + { + _logException?.Invoke(ex); + return false; + } + return true; + } + } +} \ No newline at end of file diff --git a/VAR.HttpServer/HttpUtility.cs b/VAR.HttpServer/HttpUtility.cs new file mode 100644 index 0000000..6c24ff7 --- /dev/null +++ b/VAR.HttpServer/HttpUtility.cs @@ -0,0 +1,127 @@ +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace VAR.HttpServer +{ + public sealed class HttpUtility + { + public static string UrlDecode(string str, Encoding e = null) + { + if (null == str) { return null; } + + if (str.IndexOf('%') == -1 && str.IndexOf('+') == -1) + { + return str; + } + + if (e == null) + { + e = Encoding.UTF8; + } + + long len = str.Length; + var bytes = new List(); + int xchar; + char ch; + + for (int i = 0; i < len; i++) + { + ch = str[i]; + if (ch == '%' && i + 2 < len && str[i + 1] != '%') + { + if (str[i + 1] == 'u' && i + 5 < len) + { + xchar = HexToInt(str, i + 2, 4); + if (xchar != -1) + { + WriteCharBytes(bytes, (char)xchar, e); + i += 5; + } + else + { + WriteCharBytes(bytes, '%', e); + } + } + else if ((xchar = HexToInt(str, i + 1, 2)) != -1) + { + WriteCharBytes(bytes, (char)xchar, e); + i += 2; + } + else + { + WriteCharBytes(bytes, '%', e); + } + continue; + } + + if (ch == '+') + { + WriteCharBytes(bytes, ' ', e); + } + else + { + WriteCharBytes(bytes, ch, e); + } + } + + byte[] buf = bytes.ToArray(); + bytes = null; + return e.GetString(buf); + + } + + private static void WriteCharBytes(IList buf, char ch, Encoding e) + { + if (ch > 255) + { + foreach (byte b in e.GetBytes(char.ToString(ch))) + { + buf.Add(b); + } + } + else + { + buf.Add((byte)ch); + } + } + + private static int HexDigitToInt(byte b) + { + char c = (char)b; + if (c >= '0' && c <= '9') + { + return c - '0'; + } + + if (c >= 'a' && c <= 'f') + { + return c - 'a' + 10; + } + + if (c >= 'A' && c <= 'F') + { + return c - 'A' + 10; + } + + return -1; + } + + private static int HexToInt(string str, int offset, int length) + { + int val = 0; + int end = length + offset; + for (int i = offset; i < end; i++) + { + char c = str[i]; + if (c > 127) { return -1; } + + int current = HexDigitToInt((byte)c); + if (current == -1) { return -1; } + val = (val << 4) + current; + } + + return val; + } + } +} diff --git a/VAR.HttpServer/IHttpHandler.cs b/VAR.HttpServer/IHttpHandler.cs new file mode 100644 index 0000000..fc742ad --- /dev/null +++ b/VAR.HttpServer/IHttpHandler.cs @@ -0,0 +1,7 @@ +namespace VAR.HttpServer +{ + public interface IHttpHandler + { + void HandleRequest(HttpProcessor processor); + } +} \ No newline at end of file diff --git a/VAR.HttpServer/Properties/AssemblyInfo.cs b/VAR.HttpServer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..06f09d8 --- /dev/null +++ b/VAR.HttpServer/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("VAR.HttpServer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("VAR")] +[assembly: AssemblyProduct("VAR.HttpServer")] +[assembly: AssemblyCopyright("Copyright © VAR 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: Guid("e865ba90-11f7-46d4-b718-a536b0b76c35")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/VAR.HttpServer/VAR.HttpServer.csproj b/VAR.HttpServer/VAR.HttpServer.csproj new file mode 100644 index 0000000..b12f637 --- /dev/null +++ b/VAR.HttpServer/VAR.HttpServer.csproj @@ -0,0 +1,51 @@ + + + + + Debug + AnyCPU + {E865BA90-11F7-46D4-B718-A536B0B76C35} + Library + Properties + VAR.HttpServer + VAR.HttpServer + v4.6.1 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + \ No newline at end of file