HttpServer, HttpProcessor and IHttpHandler.

This commit is contained in:
2019-04-28 17:21:01 +02:00
commit 33ee2d4a47
11 changed files with 721 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
*.exe
*.pdb
*/bin/*
*/obj/*
*.csproj.user
*.suo
.vs

View File

@@ -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!");
}
}
}
}

View File

@@ -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")]

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{6BD47B75-3DC2-4559-9311-E3D8117D4A89}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>VAR.HttpServer.MiniServerTest</RootNamespace>
<AssemblyName>VAR.HttpServer.MiniServerTest</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\VAR.HttpServer\VAR.HttpServer.csproj">
<Project>{e865ba90-11f7-46d4-b718-a536b0b76c35}</Project>
<Name>VAR.HttpServer</Name>
</ProjectReference>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

31
VAR.HttpServer.sln Normal file
View File

@@ -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

View File

@@ -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<string> _logDebugMessage = null;
private readonly Action<Exception> _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<string, string> _httpHeaders = new Dictionary<string, string>();
public Dictionary<string, string> HttpHeader { get { return _httpHeaders; } }
private string _postString;
public string PostString { get { return _postString; } }
private Dictionary<string, string> _httpParams = new Dictionary<string, string>();
public Dictionary<string, string> HttpParams { get { return _httpParams; } }
public bool IsPostback { get { return _httpMethod.ToUpper().Equals("POST"); } }
public HttpProcessor(TcpClient s, IHttpHandler handler, Action<string> logDebugMessage, Action<Exception> 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);
}
}
}
}

View File

@@ -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<string> _logDebugMessage = null;
public Action<string> LogDegugMessage { set { _logDebugMessage = value; } }
private Action<Exception> _logException = null;
public Action<Exception> 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;
}
}
}

View File

@@ -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<byte>();
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;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace VAR.HttpServer
{
public interface IHttpHandler
{
void HandleRequest(HttpProcessor processor);
}
}

View File

@@ -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")]

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{E865BA90-11F7-46D4-B718-A536B0B76C35}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>VAR.HttpServer</RootNamespace>
<AssemblyName>VAR.HttpServer</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="HttpProcessor.cs" />
<Compile Include="HttpServer.cs" />
<Compile Include="HttpUtility.cs" />
<Compile Include="IHttpHandler.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>