using System.Collections.Concurrent; using System.IO.Compression; using System.Net; using System.Net.WebSockets; using System.Runtime.CompilerServices; using System.Text; internal static class HostProgram { private static readonly ConcurrentDictionary Modules = new(); private static readonly ConcurrentDictionary Sessions = new(); private static LoadedModule _defaultModule; private static List _historyMessage = new(); private static bool _isLoading; private static bool _isRunning; private static DateTime _lastRequestAccepted; private static int _requestIdStore = 0; private static void Main(string[] args) { Console.WriteLine("Starting..."); var tWorker = new Thread(Working); _isRunning = true; tWorker.Start(); Task.Run(ReloadConfig); Console.WriteLine("Press ENTER to Stop."); Console.ReadLine(); Console.WriteLine("Shutting down..."); _isRunning = false; tWorker.Join(); Console.WriteLine("Stopped."); Console.WriteLine(); Console.Write("Press ENTER to Exit."); Console.ReadLine(); } private static void ReloadConfig() { if (_isLoading) { Console.WriteLine("Still loading, SKIP"); return; } _isLoading = true; try { ConfigFile.Reload(); ReloadModulesInternal(); } catch (Exception e) { Console.WriteLine($"Load error: {e}"); } _isLoading = false; } private static void ReloadModulesInternal() { Modules.Clear(); _defaultModule = null; if (ConfigFile.Instance.Modules?.Any() == true) { foreach (var modEnt in ConfigFile.Instance.Modules) { Console.WriteLine($"Loading module `{modEnt.Value.DisplayText}'..."); var module = new LoadedModule { VirtualPath = modEnt.Key, DisplayText = modEnt.Value.DisplayText, DefaultDocument = modEnt.Value.DefaultDocument, EnableFallbackRoute = modEnt.Value.EnableFallbackRoute, HtmlBaseReplace = modEnt.Value.HtmlBaseReplace, Files = new Dictionary() }; if (Directory.Exists(modEnt.Value.Path)) { //load by fs var files = Directory.GetFiles(modEnt.Value.Path, "*", System.IO.SearchOption.AllDirectories); foreach (var item in files) { var k = item.Substring(modEnt.Value.Path.Length + 1).Replace("\\", "/").ToLower(); module.Files[k] = File.ReadAllBytes(item); } } else if (File.Exists(modEnt.Value.Path)) { //load by package using var arc = SharpCompress.Archives.ArchiveFactory.Open(modEnt.Value.Path); foreach (var ent in arc.Entries.Where(p => p.IsDirectory == false)) { var buf = new byte[ent.Size]; using var s = ent.OpenEntryStream(); var r = s.Read(buf, 0, buf.Length); module.Files[ent.Key.ToLower()] = buf; } } else { Console.WriteLine("WARN: resource not found"); continue; } if (modEnt.Value.IsDefault && _defaultModule == null) _defaultModule = module; Modules[modEnt.Key] = module; Console.WriteLine($"Module `{modEnt.Value.DisplayText}' loaded."); } } } private static void Working() { var listener = new HttpListener(); listener.Prefixes.Add(ConfigFile.Instance.ListenPrefix); listener.Start(); var upTime = DateTime.Now; Console.WriteLine($"HTTP Server started, listening on {ConfigFile.Instance.ListenPrefix}"); listener.BeginGetContext(ContextGet, listener); _lastRequestAccepted = DateTime.Now; while (_isRunning) { var timeSpan = DateTime.Now - _lastRequestAccepted; var up = DateTime.Now - upTime; Console.Title = "SimWebCha" + $" UP {up.Days:00}D {up.Hours:00}H {up.Minutes:00}M {up.Seconds:00}S {up.Milliseconds:000}" + $" / " + $" LA {timeSpan.Days:00}D {timeSpan.Hours:00}H {timeSpan.Minutes:00}M {timeSpan.Seconds:00}S {timeSpan.Milliseconds:000}" ; Thread.Sleep(1000); } listener.Close(); Thread.Sleep(1000); } private static void ContextGet(IAsyncResult ar) { var listener = (HttpListener)ar.AsyncState; HttpListenerContext context; try { context = listener.EndGetContext(ar); } catch (Exception e) { Console.WriteLine(e); return; } if (_isRunning) listener.BeginGetContext(ContextGet, listener); ProcessRequest(context); } private static void ProcessRequest(HttpListenerContext context) { _lastRequestAccepted = DateTime.Now; var request = context.Request; var currentSessionId = Interlocked.Increment(ref _requestIdStore); Console.WriteLine($"Request #{currentSessionId:00000} from {request.RemoteEndPoint} {request.HttpMethod} {request.RawUrl}"); try { var requestPath = request.Url.LocalPath.ToLower(); var pathParts = (IReadOnlyList)requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (requestPath == "/" && _defaultModule != null) { if (_defaultModule.EnableFallbackRoute) context.Response.Redirect($"/modules/{_defaultModule.VirtualPath}/"); else context.Response.Redirect($"/modules/{_defaultModule.VirtualPath}/{_defaultModule.DefaultDocument}"); } else if (requestPath == "/connect" && request.IsWebSocketRequest) { var wsc = context.AcceptWebSocketAsync("swcp").Result; Console.WriteLine($"Request #{currentSessionId:00000} WebSocket Session Start"); var sck = Sessions[currentSessionId] = wsc.WebSocket; var buffer = new byte[1024]; try { var r = sck.ReceiveAsync(buffer, default).Result; var s = Encoding.UTF8.GetString(buffer, 0, r.Count); byte[][] copy; lock (_historyMessage) copy = _historyMessage.ToArray(); foreach (var item in copy) { sck.SendAsync(item, WebSocketMessageType.Text, true, default).Wait(); } BroadCast($"SYS{Environment.NewLine}" + $" Session #{currentSessionId:X4}({s}) Connected.{Environment.NewLine}" + $" Now number of online session: {Sessions.Count}"); while (true) { r = sck.ReceiveAsync(buffer, default).Result; if (r.Count == 0) { break; } else { s = Encoding.UTF8.GetString(buffer, 0, r.Count); BroadCast($"#{currentSessionId:X4}{Environment.NewLine} {s}"); } } } catch (Exception) { try { if (sck.State != WebSocketState.Aborted) { sck.CloseAsync(WebSocketCloseStatus.InternalServerError, "Error", default).Wait(); } } catch (Exception e) { Console.WriteLine(e); } } } else if (requestPath.StartsWith("/modules/") && pathParts.Count > 1) { var moduleKey = pathParts[1]; if (Modules.TryGetValue(moduleKey, out var module)) { var entPath = string.Join("/", pathParts.Skip(2)); void Output(byte[] bin) { if (entPath.ToLower().EndsWith(".js")) context.Response.ContentType = "application/javascript"; else if (module.HtmlBaseReplace != null && entPath.ToLower().EndsWith(".html")) { //base replace var html = Encoding.UTF8.GetString(bin); var r = html.Replace(module.HtmlBaseReplace, $""); bin = Encoding.UTF8.GetBytes(r); } context.Response.OutputStream.Write(bin, 0, bin.Length); } if (module.Files.TryGetValue(entPath, out var bin)) { Output(bin); } else if (module.EnableFallbackRoute && module.Files.TryGetValue(module.DefaultDocument, out var defBin)) { entPath = module.DefaultDocument; Output(defBin); } else context.Response.StatusCode = 404; } else context.Response.StatusCode = 404; } else if (requestPath == "/admin/" && false == request.QueryString.AllKeys.Contains("action")) { var sb = new StringBuilder(); sb.Append(""); sb.Append($" Admin - {ConfigFile.Instance.Title} "); sb.Append(""); sb.Append($"

Admin

"); sb.Append(""); sb.Append($"
"); sb.Append($"Password: "); sb.Append($"
"); sb.Append($"Operation: "); sb.Append($" "); sb.Append($"
"); context.WriteTextUtf8(sb.ToString()); } else if (requestPath == "/admin/" && request.QueryString["action"] == "Reload" && request.QueryString["pass"] == ConfigFile.Instance.AdminPassword) { Task.Run(ReloadConfig); context.Response.Redirect("/"); } else if (requestPath == "/admin/") { context.Response.Redirect("/"); } else { context.Response.StatusCode = 404; } } catch (Exception e) { Console.WriteLine(e); try { context.Response.StatusCode = 500; } catch (Exception exception) { Console.WriteLine(exception); } } finally { try { if (request.IsWebSocketRequest) { Sessions.Remove(currentSessionId, out _); BroadCast($"SYS{Environment.NewLine}" + $" Session #{currentSessionId:X4} Disconnected.{Environment.NewLine}" + $" Now number of online session: {Sessions.Count}"); } Console.WriteLine($"Request #{currentSessionId:0000} ends with status code: {context.Response.StatusCode}"); } catch (Exception e) { Console.WriteLine(e); } try { context.Response.Close(); } catch (Exception e) { Console.WriteLine(e); } } } private static void BroadCast(string content) { var now = DateTime.Now; string text = $"{now} {content}"; var buf = Encoding.UTF8.GetBytes(text); lock (_historyMessage) { _historyMessage.Add(buf); while (_historyMessage.Count >= ConfigFile.Instance.HistoryMessageLength) { _historyMessage.RemoveAt(0); } } if (Sessions.Count == 0) return; foreach (var item in Sessions) { try { if (item.Value.State == WebSocketState.Open) { item.Value.SendAsync(buf, WebSocketMessageType.Text, true, default); } else { Sessions.Remove(item.Key, out _); BroadCast($"SYS{Environment.NewLine}" + $" Session #{item.Key:X4} Disconnected.{Environment.NewLine}" + $" Now number of online session: {Sessions.Count}"); } } catch (Exception e) { Console.WriteLine(e); } } } public static void WriteTextUtf8(this HttpListenerContext context, string content, string contentType = "text/html") { var bytes = Encoding.UTF8.GetBytes(content); context.Response.ContentEncoding = Encoding.UTF8; context.Response.ContentType = contentType; if (true == context.Request.Headers["Accept-Encoding"]?.Contains("gzip")) { context.Response.AddHeader("Content-Encoding", "gzip"); var memoryStream = new MemoryStream(bytes); var gZipStream = new GZipStream(context.Response.OutputStream, CompressionMode.Compress, false); memoryStream.CopyTo(gZipStream); gZipStream.Flush(); } else { context.Response.OutputStream.Write(bytes); } } } internal class LoadedModule { public string VirtualPath { get; set; } public string DisplayText { get; set; } public string DefaultDocument { get; set; } public Dictionary Files { get; set; } public bool EnableFallbackRoute { get; set; } public string HtmlBaseReplace { get; set; } }