|
@@ -0,0 +1,423 @@
|
|
|
+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<string, LoadedModule> Modules = new();
|
|
|
+ private static readonly ConcurrentDictionary<int, WebSocket> Sessions = new();
|
|
|
+
|
|
|
+ private static LoadedModule _defaultModule;
|
|
|
+ private static List<byte[]> _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<string, byte[]>()
|
|
|
+ };
|
|
|
+
|
|
|
+ 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<string>)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, $"<base href=\"/modules/{moduleKey}/\" />");
|
|
|
+ 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("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
|
|
|
+ sb.Append($"<title> Admin - {ConfigFile.Instance.Title} </title>");
|
|
|
+ sb.Append("<body bgColor=skyBlue>");
|
|
|
+ sb.Append($"<h3>Admin</h3>");
|
|
|
+ sb.Append("<div><a href=/>Back to home</a></div>");
|
|
|
+ sb.Append($"<form method=GET>");
|
|
|
+ sb.Append($"Password: <input type=password name=pass />");
|
|
|
+ sb.Append($"<br/>");
|
|
|
+ sb.Append($"Operation: ");
|
|
|
+ sb.Append($"<input type=submit name=action value=Reload /> ");
|
|
|
+ sb.Append($"</form>");
|
|
|
+
|
|
|
+ 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<string, byte[]> Files { get; set; }
|
|
|
+ public bool EnableFallbackRoute { get; set; }
|
|
|
+ public string HtmlBaseReplace { get; set; }
|
|
|
+}
|