Skip to content

Instantly share code, notes, and snippets.

@FoxCouncil
Created September 5, 2019 04:01
Show Gist options
  • Save FoxCouncil/443b364ffbc386403eaf77306a7196a8 to your computer and use it in GitHub Desktop.
Save FoxCouncil/443b364ffbc386403eaf77306a7196a8 to your computer and use it in GitHub Desktop.
namespace FoxCouncil.Gist
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
public delegate void WebRequest(HttpListenerContext context);
public delegate void WebsocketData(WebsocketFrame frame);
public class HttpServer
{
private const string Rfc6455Guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
private const string HttpEol = "\r\n";
private const string Localhost = "localhost";
private readonly byte[] WebsocketGetHeader = new byte[3] { 71, 69, 84 };
private List<TcpClient> _websocketClients = new List<TcpClient>();
private HttpListener _listener;
private TcpListener _websocket;
private Thread _webserverThread;
private Thread _websocketThread;
private ushort _urlPort;
private ushort _websocketPort;
private string _previousLogLine;
private uint _previousLogLineCount = 0;
public bool EnableLoggingToConsole { get; set; } = true;
public bool EnableWebsockets { get; set; } = true;
public bool EnableWebserver { get; set; } = true;
public event WebRequest OnWebRequest;
public event WebsocketData OnWebsocketData;
public void Start(ushort port)
{
_urlPort = port;
_websocketPort = (ushort)(_urlPort + 1);
CreateWebsocketHandler();
CreateWebserverHandler();
}
public void Stop()
{
if (_websocketThread != null && _websocketThread.IsAlive)
{
_websocketThread.Abort();
}
if (_webserverThread != null && _webserverThread.IsAlive)
{
_webserverThread.Abort();
}
}
public void SendWebsocketFrame(WebsocketFrame socketFrame)
{
foreach (var client in _websocketClients)
{
if (client.Connected)
{
socketFrame.Send(client);
}
}
}
public static string GetMimeTypeByFilename(string filename)
{
var fileInfo = new FileInfo(filename);
switch (fileInfo.Extension)
{
case ".css":
{
return "text/css";
}
case ".js":
{
return "text/javascript";
}
case ".ico":
{
return "image/x-icon";
}
case ".ttf":
{
return "application/octet-stream";
}
default:
{
return "text/plain";
}
}
}
private void CreateWebsocketHandler()
{
if (!EnableWebsockets || _websocketThread != null)
{
return;
}
_websocketThread = new Thread(async () =>
{
_websocket = new TcpListener(IPAddress.Parse("127.0.0.1"), _websocketPort);
_websocket.Start();
// wait for socket to connect
while (!_listener.IsListening)
{
Thread.Sleep(1);
}
while (_listener.IsListening)
{
var client = await _websocket.AcceptTcpClientAsync();
lock (_websocket)
{
_websocketClients.Add(client);
}
var clientHandlerThread = new Thread(HandleWebsocketClient);
clientHandlerThread.Name = $"ws://{client.Client.RemoteEndPoint}/";
clientHandlerThread.Start(client);
}
_websocket.Stop();
});
_websocketThread.Name = "Websocket Server";
_websocketThread.Start();
Console.WriteLine($"Websocket Started: ws://{Localhost}:{_websocketPort}/");
}
private void HandleWebsocketClient(object clientObj)
{
if (!(clientObj is TcpClient client))
{
throw new ApplicationException("The websocket client object was null in the HandleWebsocketClient method.");
}
while (client.Connected)
{
while (client.Available == 0 && client.Connected) ;
var bytes = new byte[client.Available];
try
{
client.Client.Receive(bytes);
}
catch (SocketException)
{
return;
}
if (WebsocketGetHeader.Except(bytes).Count() == 0)
{
WebsocketSwitchProtocol(client, bytes);
}
else
{
var frame = new WebsocketFrame(bytes);
if (!frame.Valid)
{
client.Close();
}
else
{
OnWebsocketData?.Invoke(frame);
}
}
}
lock (_websocket)
{
_websocketClients.Remove(client);
}
}
private void CreateWebserverHandler()
{
if (!EnableWebserver || _webserverThread != null)
{
return;
}
var url = $"http://{Localhost}:{_urlPort}/";
_webserverThread = new Thread(async () =>
{
_listener = new HttpListener();
_listener.Prefixes.Add(url);
_listener.Start();
var context = await _listener.GetContextAsync();
await HandleContext(context);
});
_webserverThread.Name = "Web Server";
_webserverThread.Start();
Console.WriteLine($"Webserver Started: {url}");
}
private async Task HandleContext(HttpListenerContext result)
{
OnWebRequest?.Invoke(result);
if (EnableLoggingToConsole)
{
var filename = result.Request.Url.LocalPath.ToLower();
var sourceIP = result.Request.RemoteEndPoint.ToString();
var httpCode = result.Response.StatusCode;
var responseSize = result.Response.ContentLength64;
var responseType = result.Response.ContentType;
var timeString = $"{DateTimeOffset.Now.ToString("O")}: ";
var logLine = $"[{sourceIP}] ({httpCode}) {responseSize,-6} {filename,-25} {responseType}";
if (logLine.Equals(_previousLogLine))
{
var leftOffest = logLine.Length + timeString.Length + 1;
var oldCursorTop = Console.CursorTop;
var newCursorTop = oldCursorTop - 1;
Console.SetCursorPosition(0, newCursorTop);
Console.Write(timeString);
Console.SetCursorPosition(leftOffest, newCursorTop);
Console.Write($"x{++_previousLogLineCount}");
Console.SetCursorPosition(0, oldCursorTop);
}
else
{
_previousLogLine = logLine;
_previousLogLineCount = 0;
Console.WriteLine(timeString + logLine);
}
}
var context = await _listener.GetContextAsync();
await HandleContext(context);
}
private static void WebsocketSwitchProtocol(TcpClient client, byte[] bytes)
{
var headerString = Encoding.UTF8.GetString(bytes);
var websocketKey = new Regex("Sec-WebSocket-Key: (.*)").Match(headerString).Groups[1].Value.Trim();
var encodedHash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(websocketKey + Rfc6455Guid));
var websocketAcceptHash = Convert.ToBase64String(encodedHash);
var response = Encoding.UTF8.GetBytes(
"HTTP/1.1 101 Switching Protocols" + HttpEol
+ "Connection: Upgrade" + HttpEol
+ "Upgrade: websocket" + HttpEol
+ "Sec-WebSocket-Accept: " + websocketAcceptHash + HttpEol
+ HttpEol);
client.Client.Send(response);
}
}
public static class HttpServerExtensions
{
public static void Json(this HttpListenerContext context, object obj = null)
{
context.Ok(obj == null ? "{ \"result\": true }" : JsonConvert.SerializeObject(obj), "application/json");
}
public static void Ok(this HttpListenerContext context, string content, string mimeType = "text/html")
{
byte[] buffer = Encoding.UTF8.GetBytes(content);
context.Ok(buffer, mimeType);
}
public static void Ok(this HttpListenerContext context, byte[] content, string mimeType = "text/html")
{
context.Response.AddHeader("Content-Type", mimeType);
context.Response.AddHeader("Server", "FoxCouncil HTTP Server 1.0");
WriteBuffer(context, content);
}
public static void NotFound(this HttpListenerContext context)
{
byte[] buffer = Encoding.UTF8.GetBytes("404 Not Found");
context.Response.AddHeader("Content-Type", "text/plain");
context.Response.AddHeader("Server", "FoxCouncil HTTP Server 1.0");
context.Response.StatusCode = 404;
WriteBuffer(context, buffer);
}
private static void WriteBuffer(HttpListenerContext context, byte[] buffer)
{
context.Response.ContentLength64 = buffer.Length;
context.Response.OutputStream.Write(buffer, 0, buffer.Length);
context.Response.OutputStream.Close();
}
}
public class WebsocketFrame
{
public bool Valid { get; private set; }
public bool FIN { get; private set; } = true;
public bool RSV1 { get; private set; }
public bool RSV2 { get; private set; }
public bool RSV3 { get; private set; }
public WebsocketOpcode Opcode { get; set; }
public bool Mask { get; }
public ulong Length { get; private set; }
public byte[] Binary { get; private set; }
public string Message => Binary == null ? string.Empty : Encoding.UTF8.GetString(Binary);
public WebsocketFrame(string message)
{
Opcode = WebsocketOpcode.Text;
Binary = Encoding.UTF8.GetBytes(message);
Length = (ulong)Binary.LongLength;
}
public WebsocketFrame(byte[] bytes)
{
var byteIdx = 2;
var firstByte = bytes[0];
FIN = (firstByte & 0b10000000) != 0;
RSV1 = (firstByte & 0b01000000) != 0;
RSV2 = (firstByte & 0b00100000) != 0;
RSV3 = (firstByte & 0b00010000) != 0;
if (RSV1 || RSV2 || RSV3)
{
Valid = false;
return;
}
Opcode = (WebsocketOpcode)(firstByte & 0b00001111);
if (Opcode == WebsocketOpcode.ConnectionClose)
{
Valid = false;
return;
}
var secondByte = bytes[1];
Mask = (secondByte & 0b10000000) != 0;
var lengthVal = secondByte - 128;
var lengthByteArray = new byte[8];
if (lengthVal < 126)
{
lengthByteArray[0] = Convert.ToByte(lengthVal);
}
else if (lengthVal == 126)
{
var sixteenBitArray = new byte[2];
sixteenBitArray[0] = bytes[byteIdx++];
sixteenBitArray[1] = bytes[byteIdx++];
if (BitConverter.IsLittleEndian)
{
Array.Reverse(sixteenBitArray);
}
lengthByteArray[0] = sixteenBitArray[0];
lengthByteArray[1] = sixteenBitArray[1];
}
else if (lengthVal == 127)
{
lengthByteArray[0] = bytes[byteIdx++];
lengthByteArray[1] = bytes[byteIdx++];
lengthByteArray[2] = bytes[byteIdx++];
lengthByteArray[3] = bytes[byteIdx++];
lengthByteArray[4] = bytes[byteIdx++];
lengthByteArray[5] = bytes[byteIdx++];
lengthByteArray[6] = bytes[byteIdx++];
lengthByteArray[7] = bytes[byteIdx++];
if (BitConverter.IsLittleEndian)
{
Array.Reverse(lengthByteArray);
}
}
Length = BitConverter.ToUInt64(lengthByteArray);
if ((Opcode == WebsocketOpcode.Binary || Opcode == WebsocketOpcode.Text) && Mask)
{
var masks = new byte[4] { bytes[byteIdx++], bytes[byteIdx++], bytes[byteIdx++], bytes[byteIdx++] };
Binary = new byte[Length];
for (var i = 0; i < (int)Length; ++i)
{
Binary[i] = (byte)(bytes[byteIdx + i] ^ masks[i % 4]);
}
Valid = true;
}
else if (!Mask)
{
Valid = false;
}
}
public void Send(TcpClient client)
{
var frameLength = Binary.LongLength + 2;
int payloadLength;
if (Binary.LongLength > 126 && Binary.LongLength <= ushort.MaxValue)
{
payloadLength = 126;
frameLength += 2;
}
else if (Binary.LongLength > ushort.MaxValue)
{
payloadLength = 127;
frameLength += 8;
}
else
{
payloadLength = Binary.Length;
}
var frameBytes = new byte[frameLength];
frameBytes[0] |= (byte)(Convert.ToInt32(FIN) << 7);
frameBytes[0] |= (byte)(Convert.ToInt32(RSV1) << 6);
frameBytes[0] |= (byte)(Convert.ToInt32(RSV2) << 5);
frameBytes[0] |= (byte)(Convert.ToInt32(RSV3) << 4);
frameBytes[0] |= (byte)((int)Opcode & 0b00001111);
frameBytes[1] |= (byte)(Convert.ToInt32(Mask) << 7);
frameBytes[1] |= (byte)(payloadLength & 0b01111111);
var offset = 2;
if (payloadLength == 126)
{
frameBytes[2] = (byte)(Binary.Length >> 8);
frameBytes[3] = (byte)Binary.Length;
offset = 4;
}
else if (payloadLength == 127)
{
frameBytes[2] = (byte)(Binary.LongLength >> 56);
frameBytes[3] = (byte)(Binary.LongLength >> 48);
frameBytes[4] = (byte)(Binary.LongLength >> 40);
frameBytes[5] = (byte)(Binary.LongLength >> 32);
frameBytes[6] = (byte)(Binary.LongLength >> 24);
frameBytes[7] = (byte)(Binary.LongLength >> 16);
frameBytes[8] = (byte)(Binary.LongLength >> 8);
frameBytes[9] = (byte)Binary.LongLength;
offset = 10;
}
Binary.CopyTo(frameBytes, offset);
try
{
client.Client.Send(frameBytes);
}
catch (SocketException)
{
// Noop
}
}
public enum WebsocketOpcode
{
Continue = 0,
Text = 1,
Binary = 2,
ConnectionClose = 8,
Ping = 9,
Pong = 10
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment