Last active
October 18, 2018 20:44
-
-
Save Quacky2200/ed7eed524488b4247f9421c7f8ed55b5 to your computer and use it in GitHub Desktop.
Do you ever need a tiny C# web server in your back pocket?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading; | |
using System.Net; | |
using System.Diagnostics; | |
using System.Net.Sockets; | |
// Created a temporary web server as part of a payment SDK example | |
// This is due to HttpListener needing admin privileges or otherwise | |
// with the need of an ASP.NET project | |
public class WebServer | |
{ | |
private TcpListener server; | |
private Boolean Quit = false; | |
private Thread listener; | |
private const int BUFFER_SIZE = 1024; | |
List<Thread> Threads = new List<Thread>(); | |
public delegate void OnRequestHandler(WebServer sender, Request Request); | |
public event OnRequestHandler OnRequest; | |
public class Request | |
{ | |
public Dictionary<String, String> Headers = new Dictionary<string, string>(); | |
public string Method; | |
public string Path; | |
public string QueryString; | |
public string HttpVersion; | |
public string Body; | |
public Socket Client; | |
} | |
public class Response | |
{ | |
public int Status = 200; | |
private string StatusName = ""; | |
public string ContentType = "text/plain"; | |
public byte[] Content; | |
public Dictionary<string, dynamic> Headers = new Dictionary<string, dynamic>(); | |
public byte[] GetBytes() | |
{ | |
ResolveStatus(); | |
string HttpMessage = String.Format("{0} {1} {2}", "HTTP/1.1", Status, StatusName) + "\r\n"; | |
List<string> HeaderStrings = new List<string>(); | |
List<string> HeaderKeys = Headers.Keys.ToList(); | |
for (int i = 0; i < HeaderKeys.Count; i++) | |
{ | |
dynamic Obj = Headers[HeaderKeys[i]]; | |
Headers.Remove(HeaderKeys[i]); | |
Headers.Add(HeaderKeys[i].ToLower(), Obj); | |
} | |
if (!Headers.ContainsKey("Content-Type")) | |
{ | |
Headers.Add("Content-Type", ContentType); | |
} | |
Headers.Add("Content-Length", Content.Length); | |
Headers.Add("Server", "dotnet"); | |
Headers.Add("Content-Disposition", "inline"); | |
Headers.Add("Connection", "close"); | |
foreach (KeyValuePair<string, dynamic> KVP in Headers) | |
{ | |
HeaderStrings.Add(KVP.Key + ": " + KVP.Value + "\r\n"); | |
} | |
HttpMessage += String.Join("", HeaderStrings) + "\r\n"; | |
List<byte> HttpBytes = Encoding.ASCII.GetBytes(HttpMessage).ToList(); | |
HttpBytes.AddRange(Content); | |
return HttpBytes.ToArray(); | |
} | |
protected void ResolveStatus() | |
{ | |
Dictionary<int, string> codes = new Dictionary<int, string> | |
{ | |
[200] = "OK", | |
[500] = "Internal Server Error", | |
[401] = "Bad Request", | |
[403] = "Forbidden" | |
}; | |
if (!codes.ContainsKey(Status)) | |
{ | |
Status = 500; | |
StatusName = codes[500]; | |
Content = Encoding.ASCII.GetBytes(StatusName); | |
// More should be added, add codes above when needed ^ | |
Debug.WriteLine("Invalid server HTTP code"); | |
} else | |
{ | |
StatusName = codes[Status]; | |
} | |
} | |
public void SetText(String Text) | |
{ | |
Content = Encoding.ASCII.GetBytes(Text); | |
} | |
public void SetHtml(String Html) | |
{ | |
Content = Encoding.ASCII.GetBytes(Html); | |
ContentType = "text/html"; | |
} | |
public static Response SendText(String Text) | |
{ | |
Response Result = new Response(); | |
Result.SetText(Text); | |
return Result; | |
} | |
public static Response SendHtml(String Text) | |
{ | |
Response Result = new Response(); | |
Result.SetHtml(Text); | |
return Result; | |
} | |
} | |
public WebServer(int port) | |
{ | |
server = new System.Net.Sockets.TcpListener(IPAddress.Loopback, port); | |
} | |
protected void MoveToNewProcessingThread(Socket Client) | |
{ | |
Thread Current = new Thread(() => | |
{ | |
string Content; | |
List<byte> Bytes = new List<byte>(); | |
byte[] Data; | |
int Size = BUFFER_SIZE; | |
while (Size >= BUFFER_SIZE) | |
{ | |
Data = new byte[BUFFER_SIZE]; | |
Size = Client.Receive(Data); | |
Bytes.AddRange(Data); | |
} | |
Content = Encoding.ASCII.GetString(Bytes.ToArray()).Replace("\0", ""); | |
if (Content == "") | |
{ | |
// Unsure exactly what to do in this use case. | |
Client.Close(); | |
return; | |
} | |
List<string> HttpMessage = Content.Split(Char.Parse("\n")).ToList(); | |
Request Request = new Request(); | |
string[] Action = HttpMessage[0].TrimEnd(Char.Parse("\r")).Split(Char.Parse(" ")); | |
if (Action.Length > 3) | |
{ | |
throw new InvalidOperationException("Too many fields in HTTP1/1 spec?"); | |
} | |
int Count = HttpMessage.Count; | |
for (int i = 1; i < Count; i++) | |
{ | |
if (HttpMessage[i] == "\r") | |
{ | |
HttpMessage.RemoveRange(0, i + 1); | |
break; | |
} | |
string[] Header = HttpMessage[i].TrimEnd(Char.Parse("\r")).Split(Char.Parse(":")); | |
string key = Header[0]; | |
Header[0] = ""; | |
Request.Headers.Add(key, String.Join(":", Header).TrimStart(Char.Parse(":")).TrimStart(Char.Parse(" "))); | |
} | |
Request.Method = Action[0].ToUpper(); | |
string[] Path = Action[1].Split(Char.Parse("?")); | |
Request.Path = Path[0]; | |
Path[0] = ""; | |
Request.QueryString = String.Join("?", Path).TrimStart(Char.Parse("?")); | |
Request.HttpVersion = Action[2]; | |
Request.Body = String.Join("\n", HttpMessage.ToArray()); | |
HttpMessage = null; | |
Content = null; | |
Request.Client = Client; | |
if (OnRequest != null) | |
{ | |
OnRequest(this, Request); | |
} else | |
{ | |
Send(Client, new Response() { Status = 500, Content = Encoding.ASCII.GetBytes("Internal Server Error") }); | |
} | |
}); | |
Current.Start(); | |
Threads.Add(Current); | |
} | |
public void Send(Socket Client, Response Response) | |
{ | |
List<byte> Content = Response.GetBytes().ToList(); | |
int OriginalSize = Content.Count; | |
int Size = BUFFER_SIZE; | |
int Count = 0; | |
while(Content.Count > 0) | |
{ | |
// Try to send as much as our buffer size allows | |
byte[] Chunk = Content.GetRange(0, Math.Min(BUFFER_SIZE, Content.Count)).ToArray(); | |
// Get how much the client has received | |
Size = Client.Send(Chunk); | |
// Only remove what the client was able to accept | |
// (so that anything left over can be tried again) | |
Content.RemoveRange(0, Size); | |
Count++; | |
if (Count > (OriginalSize * 4)) | |
{ | |
// We've been here to long. Stop! | |
Client.Close(); | |
Debug.WriteLine("Client Timeout?"); | |
return; | |
} | |
} | |
Client.Close(); | |
} | |
public void Start() | |
{ | |
// Main listener thread | |
listener = new Thread(() => | |
{ | |
server.Start(); | |
while (!Quit) | |
{ | |
Socket Client = null; | |
try | |
{ | |
Client = server.AcceptSocket(); | |
} catch (/*Socket*/Exception e) | |
{ | |
// Ignore when quiting since the socket | |
// will have to be interrupted | |
if (!Quit) | |
{ | |
Debug.WriteLine(e.Message); | |
return; | |
} | |
} | |
if (Client != null) | |
{ | |
MoveToNewProcessingThread(Client); | |
} | |
} | |
}); | |
listener.Start(); | |
} | |
public void Stop() | |
{ | |
Quit = true; | |
foreach(Thread Thread in Threads) | |
{ | |
Thread.Abort(); | |
} | |
server.Stop(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment