Created
September 5, 2019 04:01
-
-
Save FoxCouncil/443b364ffbc386403eaf77306a7196a8 to your computer and use it in GitHub Desktop.
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
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