HostProgram.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. using SimpleWebChat.ConHost;
  2. using System.Collections.Concurrent;
  3. using System.IO.Compression;
  4. using System.Net;
  5. using System.Text;
  6. internal static class HostProgram
  7. {
  8. private static readonly ConcurrentDictionary<string, LoadedModule> Modules = new();
  9. private static LoadedModule _defaultModule;
  10. private static bool _isLoading;
  11. private static bool _isRunning;
  12. private static DateTime _lastRequestAccepted;
  13. private static int _requestIdStore = 0;
  14. private static void Main(string[] args)
  15. {
  16. Console.WriteLine("Starting...");
  17. var tWorker = new Thread(Working);
  18. _isRunning = true;
  19. tWorker.Start();
  20. Task.Run(ReloadConfig);
  21. Console.WriteLine("Press ENTER to Stop.");
  22. Console.ReadLine();
  23. Console.WriteLine("Shutting down...");
  24. _isRunning = false;
  25. tWorker.Join();
  26. Console.WriteLine("Stopped.");
  27. Console.WriteLine();
  28. Console.Write("Press ENTER to Exit.");
  29. Console.ReadLine();
  30. }
  31. private static void ReloadConfig()
  32. {
  33. if (_isLoading)
  34. {
  35. Console.WriteLine("Still loading, SKIP");
  36. return;
  37. }
  38. _isLoading = true;
  39. try
  40. {
  41. ConfigFile.Reload();
  42. ReloadModulesInternal();
  43. }
  44. catch (Exception e)
  45. {
  46. Console.WriteLine($"Load error: {e}");
  47. }
  48. _isLoading = false;
  49. }
  50. private static void ReloadModulesInternal()
  51. {
  52. Modules.Clear();
  53. _defaultModule = null;
  54. if (ConfigFile.Instance.Modules?.Any() == true)
  55. {
  56. foreach (var modEnt in ConfigFile.Instance.Modules)
  57. {
  58. Console.WriteLine($"Loading module `{modEnt.Value.DisplayText}'...");
  59. var module = new LoadedModule
  60. {
  61. VirtualPath = modEnt.Key,
  62. DisplayText = modEnt.Value.DisplayText,
  63. DefaultDocument = modEnt.Value.DefaultDocument,
  64. EnableFallbackRoute = modEnt.Value.EnableFallbackRoute,
  65. HtmlBaseReplace = modEnt.Value.HtmlBaseReplace,
  66. Files = new Dictionary<string, byte[]>()
  67. };
  68. if (Directory.Exists(modEnt.Value.Path))
  69. {
  70. //load by fs
  71. var files = Directory.GetFiles(modEnt.Value.Path, "*", System.IO.SearchOption.AllDirectories);
  72. foreach (var item in files)
  73. {
  74. var k = item.Substring(modEnt.Value.Path.Length + 1).Replace("\\", "/").ToLower();
  75. module.Files[k] = File.ReadAllBytes(item);
  76. }
  77. }
  78. else if (File.Exists(modEnt.Value.Path))
  79. {
  80. //load by package
  81. using var arc = SharpCompress.Archives.ArchiveFactory.Open(modEnt.Value.Path);
  82. foreach (var ent in arc.Entries.Where(p => p.IsDirectory == false))
  83. {
  84. var buf = new byte[ent.Size];
  85. using var s = ent.OpenEntryStream();
  86. var r = s.Read(buf, 0, buf.Length);
  87. module.Files[ent.Key.ToLower()] = buf;
  88. }
  89. }
  90. else
  91. {
  92. Console.WriteLine("WARN: resource not found");
  93. continue;
  94. }
  95. if (modEnt.Value.IsDefault && _defaultModule == null) _defaultModule = module;
  96. Modules[modEnt.Key] = module;
  97. Console.WriteLine($"Module `{modEnt.Value.DisplayText}' loaded.");
  98. }
  99. }
  100. }
  101. private static void Working()
  102. {
  103. var listener = new HttpListener();
  104. listener.Prefixes.Add(ConfigFile.Instance.ListenPrefix);
  105. if (ConfigFile.Instance.AliasPrefix?.Any() == true)
  106. foreach (var prefix in ConfigFile.Instance.AliasPrefix)
  107. listener.Prefixes.Add(prefix);
  108. listener.Start();
  109. var upTime = DateTime.Now;
  110. Console.WriteLine($"HTTP Server started, listening on {string.Join("; ", listener.Prefixes)}");
  111. listener.BeginGetContext(ContextGet, listener);
  112. _lastRequestAccepted = DateTime.Now;
  113. while (_isRunning)
  114. {
  115. var timeSpan = DateTime.Now - _lastRequestAccepted;
  116. var up = DateTime.Now - upTime;
  117. Console.Title =
  118. "SimWebChat"
  119. + $" UP {up.Days:00}D {up.Hours:00}H {up.Minutes:00}M {up.Seconds:00}S {up.Milliseconds:000}"
  120. + $" / "
  121. + $" LA {timeSpan.Days:00}D {timeSpan.Hours:00}H {timeSpan.Minutes:00}M {timeSpan.Seconds:00}S {timeSpan.Milliseconds:000}"
  122. ;
  123. Thread.Sleep(1000);
  124. }
  125. listener.Close();
  126. Thread.Sleep(1000);
  127. }
  128. private static void ContextGet(IAsyncResult ar)
  129. {
  130. var listener = (HttpListener)ar.AsyncState;
  131. HttpListenerContext context;
  132. try
  133. {
  134. context = listener.EndGetContext(ar);
  135. }
  136. catch (Exception e)
  137. {
  138. Console.WriteLine(e);
  139. return;
  140. }
  141. if (_isRunning) listener.BeginGetContext(ContextGet, listener);
  142. ProcessRequest(context);
  143. }
  144. private static void ProcessRequest(HttpListenerContext context)
  145. {
  146. _lastRequestAccepted = DateTime.Now;
  147. var request = context.Request;
  148. var currentSessionId = Interlocked.Increment(ref _requestIdStore);
  149. Console.WriteLine($"Request #{currentSessionId:00000} from {request.RemoteEndPoint} {request.HttpMethod} {request.RawUrl}");
  150. try
  151. {
  152. var requestPath = request.Url.LocalPath.ToLower();
  153. var pathParts = (IReadOnlyList<string>)requestPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
  154. if (requestPath == "/" && _defaultModule != null)
  155. {
  156. if (_defaultModule.EnableFallbackRoute) context.Response.Redirect($"/modules/{_defaultModule.VirtualPath}/");
  157. else context.Response.Redirect($"/modules/{_defaultModule.VirtualPath}/{_defaultModule.DefaultDocument}");
  158. }
  159. else if (requestPath == "/connect" && request.IsWebSocketRequest)
  160. {
  161. SimpleWebChatServerModule.ProcessRequest(context, currentSessionId);
  162. }
  163. else if (requestPath == "/connect/voice" && request.IsWebSocketRequest)
  164. {
  165. SimpleVoiceChatServerModule.ProcessRequest(context, currentSessionId);
  166. }
  167. else if (requestPath == "/connect/voice/meeting" && request.IsWebSocketRequest)
  168. {
  169. SimpleVoiceMeetingServerModule.ProcessRequest(context, currentSessionId);
  170. }
  171. else if (requestPath.StartsWith("/modules/") && pathParts.Count > 1)
  172. {
  173. var moduleKey = pathParts[1];
  174. if (Modules.TryGetValue(moduleKey, out var module))
  175. {
  176. var entPath = string.Join("/", pathParts.Skip(2));
  177. void Output(byte[] bin)
  178. {
  179. if (entPath.ToLower().EndsWith(".js")) context.Response.ContentType = "application/javascript";
  180. else if (module.HtmlBaseReplace != null && entPath.ToLower().EndsWith(".html"))
  181. {
  182. //base replace
  183. var html = Encoding.UTF8.GetString(bin);
  184. var r = html.Replace(module.HtmlBaseReplace, $"<base href=\"/modules/{moduleKey}/\" />");
  185. bin = Encoding.UTF8.GetBytes(r);
  186. }
  187. context.Response.OutputStream.Write(bin, 0, bin.Length);
  188. }
  189. if (module.Files.TryGetValue(entPath, out var bin))
  190. {
  191. Output(bin);
  192. }
  193. else if (module.EnableFallbackRoute && module.Files.TryGetValue(module.DefaultDocument, out var defBin))
  194. {
  195. entPath = module.DefaultDocument;
  196. Output(defBin);
  197. }
  198. else context.Response.StatusCode = 404;
  199. }
  200. else context.Response.StatusCode = 404;
  201. }
  202. else if (requestPath == "/admin/" && false == request.QueryString.AllKeys.Contains("action"))
  203. {
  204. var sb = new StringBuilder();
  205. sb.Append("<!DOCTYPE html><html lang=\"zh-cn\"><meta charset=\"UTF-8\">");
  206. sb.Append($"<title> Admin - {ConfigFile.Instance.Title} </title>");
  207. sb.Append("<body bgColor=skyBlue>");
  208. sb.Append($"<h3>Admin</h3>");
  209. sb.Append("<div><a href=/>Back to home</a></div>");
  210. sb.Append($"<form method=GET>");
  211. sb.Append($"Password: <input type=password name=pass />");
  212. sb.Append($"<br/>");
  213. sb.Append($"Operation: ");
  214. sb.Append($"<input type=submit name=action value=Reload /> ");
  215. sb.Append($"</form>");
  216. context.WriteTextUtf8(sb.ToString());
  217. }
  218. else if (requestPath == "/admin/" && request.QueryString["action"] == "Reload" && request.QueryString["pass"] == ConfigFile.Instance.AdminPassword)
  219. {
  220. Task.Run(ReloadConfig);
  221. context.Response.Redirect("/");
  222. }
  223. else if (requestPath == "/admin/")
  224. {
  225. context.Response.Redirect("/");
  226. }
  227. else
  228. {
  229. context.Response.StatusCode = 404;
  230. }
  231. }
  232. catch (Exception e)
  233. {
  234. Console.WriteLine(e);
  235. try
  236. {
  237. context.Response.StatusCode = 500;
  238. }
  239. catch (Exception exception)
  240. {
  241. Console.WriteLine(exception);
  242. }
  243. }
  244. finally
  245. {
  246. try
  247. {
  248. if (request.IsWebSocketRequest)
  249. {
  250. }
  251. Console.WriteLine($"Request #{currentSessionId:00000} ends with status code: {context.Response.StatusCode}");
  252. }
  253. catch (Exception e)
  254. {
  255. Console.WriteLine(e);
  256. }
  257. try
  258. {
  259. context.Response.Close();
  260. }
  261. catch (Exception e)
  262. {
  263. Console.WriteLine(e);
  264. }
  265. }
  266. }
  267. public static void WriteTextUtf8(this HttpListenerContext context, string content, string contentType = "text/html")
  268. {
  269. var bytes = Encoding.UTF8.GetBytes(content);
  270. context.Response.ContentEncoding = Encoding.UTF8;
  271. context.Response.ContentType = contentType;
  272. if (true == context.Request.Headers["Accept-Encoding"]?.Contains("gzip"))
  273. {
  274. context.Response.AddHeader("Content-Encoding", "gzip");
  275. var memoryStream = new MemoryStream(bytes);
  276. var gZipStream = new GZipStream(context.Response.OutputStream, CompressionMode.Compress, false);
  277. memoryStream.CopyTo(gZipStream);
  278. gZipStream.Flush();
  279. }
  280. else
  281. {
  282. context.Response.OutputStream.Write(bytes);
  283. }
  284. }
  285. }
  286. internal class LoadedModule
  287. {
  288. public string VirtualPath { get; set; }
  289. public string DisplayText { get; set; }
  290. public string DefaultDocument { get; set; }
  291. public Dictionary<string, byte[]> Files { get; set; }
  292. public bool EnableFallbackRoute { get; set; }
  293. public string HtmlBaseReplace { get; set; }
  294. }