| [391] | 1 | using System; | 
|---|
|  | 2 | using System.Collections.Generic; | 
|---|
|  | 3 | using System.Net; | 
|---|
|  | 4 | using System.Reflection; | 
|---|
|  | 5 | using System.Threading; | 
|---|
|  | 6 | using Webserver.SSE; | 
|---|
|  | 7 |  | 
|---|
|  | 8 | // Implemented following HTML spec | 
|---|
|  | 9 | // https://html.spec.whatwg.org/multipage/server-sent-events.html | 
|---|
|  | 10 |  | 
|---|
|  | 11 | namespace Webserver.UrlHandlers { | 
|---|
|  | 12 | public class SseHandler : AbsHandler { | 
|---|
|  | 13 | private readonly Dictionary<string, AbsEvent> events = new CaseInsensitiveStringDictionary<AbsEvent> (); | 
|---|
|  | 14 |  | 
|---|
|  | 15 | private ThreadManager.ThreadInfo queueThead; | 
|---|
|  | 16 | private readonly AutoResetEvent evSendRequest = new AutoResetEvent (false); | 
|---|
|  | 17 | private bool shutdown; | 
|---|
|  | 18 |  | 
|---|
|  | 19 | public SseHandler (string _moduleName = null) : base (_moduleName) { | 
|---|
|  | 20 | Type[] ctorTypes = { typeof (SseHandler) }; | 
|---|
|  | 21 | object[] ctorParams = { this }; | 
|---|
|  | 22 |  | 
|---|
|  | 23 | foreach (Type t in Assembly.GetExecutingAssembly ().GetTypes ()) { | 
|---|
|  | 24 | if (t.IsAbstract || !t.IsSubclassOf (typeof (AbsEvent))) { | 
|---|
|  | 25 | continue; | 
|---|
|  | 26 | } | 
|---|
|  | 27 |  | 
|---|
|  | 28 | ConstructorInfo ctor = t.GetConstructor (ctorTypes); | 
|---|
|  | 29 | if (ctor == null) { | 
|---|
|  | 30 | continue; | 
|---|
|  | 31 | } | 
|---|
|  | 32 |  | 
|---|
|  | 33 | AbsEvent apiInstance = (AbsEvent)ctor.Invoke (ctorParams); | 
|---|
|  | 34 | AddEvent (apiInstance.Name, apiInstance); | 
|---|
|  | 35 | } | 
|---|
|  | 36 | } | 
|---|
|  | 37 |  | 
|---|
|  | 38 | public override void SetBasePathAndParent (Web _parent, string _relativePath) { | 
|---|
|  | 39 | base.SetBasePathAndParent (_parent, _relativePath); | 
|---|
|  | 40 |  | 
|---|
|  | 41 | queueThead = ThreadManager.StartThread ("SSE-Processing_" + urlBasePath, QueueProcessThread, ThreadPriority.BelowNormal, | 
|---|
|  | 42 | _useRealThread: true); | 
|---|
|  | 43 | } | 
|---|
|  | 44 |  | 
|---|
|  | 45 | public override void Shutdown () { | 
|---|
|  | 46 | base.Shutdown (); | 
|---|
|  | 47 | shutdown = true; | 
|---|
|  | 48 | SignalSendQueue (); | 
|---|
|  | 49 | } | 
|---|
|  | 50 |  | 
|---|
|  | 51 | public void AddEvent (string _eventName, AbsEvent _eventInstance) { | 
|---|
|  | 52 | events.Add (_eventName, _eventInstance); | 
|---|
|  | 53 | WebPermissions.Instance.AddKnownModule ("webevent." + _eventName, _eventInstance.DefaultPermissionLevel ()); | 
|---|
|  | 54 | } | 
|---|
|  | 55 |  | 
|---|
|  | 56 | public override void HandleRequest (RequestContext _context) { | 
|---|
|  | 57 | string eventName = _context.RequestPath.Remove (0, urlBasePath.Length); | 
|---|
|  | 58 |  | 
|---|
|  | 59 | if (!events.TryGetValue (eventName, out AbsEvent eventInstance)) { | 
|---|
| [399] | 60 | Log.Warning ($"[Web] [SSE] In {nameof (SseHandler)}.HandleRequest(): No handler found for event \"{eventName}\""); | 
|---|
| [391] | 61 | _context.Response.StatusCode = (int)HttpStatusCode.NotFound; | 
|---|
|  | 62 | return; | 
|---|
|  | 63 | } | 
|---|
|  | 64 |  | 
|---|
|  | 65 | if (!IsAuthorizedForEvent (eventName, _context.PermissionLevel)) { | 
|---|
|  | 66 | _context.Response.StatusCode = (int)HttpStatusCode.Forbidden; | 
|---|
|  | 67 | if (_context.Connection != null) { | 
|---|
|  | 68 | //Log.Out ($"{nameof(SseHandler)}: user '{user.SteamID}' not allowed to access '{eventName}'"); | 
|---|
|  | 69 | } | 
|---|
|  | 70 |  | 
|---|
|  | 71 | return; | 
|---|
|  | 72 | } | 
|---|
|  | 73 |  | 
|---|
|  | 74 | try { | 
|---|
|  | 75 | eventInstance.AddListener (_context.Response); | 
|---|
|  | 76 |  | 
|---|
|  | 77 | // Keep the request open | 
|---|
|  | 78 | _context.Response.SendChunked = true; | 
|---|
|  | 79 |  | 
|---|
|  | 80 | _context.Response.AddHeader ("Content-Type", "text/event-stream"); | 
|---|
|  | 81 | _context.Response.OutputStream.Flush (); | 
|---|
|  | 82 | } catch (Exception e) { | 
|---|
| [399] | 83 | Log.Error ($"[Web] [SSE] In {nameof (SseHandler)}.HandleRequest(): Handler {eventInstance.Name} threw an exception:"); | 
|---|
| [391] | 84 | Log.Exception (e); | 
|---|
|  | 85 | _context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; | 
|---|
|  | 86 | } | 
|---|
|  | 87 | } | 
|---|
|  | 88 |  | 
|---|
|  | 89 | private bool IsAuthorizedForEvent (string _eventName, int _permissionLevel) { | 
|---|
|  | 90 | return WebPermissions.Instance.ModuleAllowedWithLevel ("webevent." + _eventName, _permissionLevel); | 
|---|
|  | 91 | } | 
|---|
|  | 92 |  | 
|---|
|  | 93 | private void QueueProcessThread (ThreadManager.ThreadInfo _threadInfo) { | 
|---|
|  | 94 | while (!shutdown && !_threadInfo.TerminationRequested ()) { | 
|---|
|  | 95 | evSendRequest.WaitOne (500); | 
|---|
|  | 96 |  | 
|---|
|  | 97 | foreach ((string eventName, AbsEvent eventHandler) in events) { | 
|---|
|  | 98 | try { | 
|---|
|  | 99 | eventHandler.ProcessSendQueue (); | 
|---|
|  | 100 | } catch (Exception e) { | 
|---|
| [399] | 101 | Log.Error ($"[Web] [SSE] '{eventName}': Error processing send queue"); | 
|---|
| [391] | 102 | Log.Exception (e); | 
|---|
|  | 103 | } | 
|---|
|  | 104 | } | 
|---|
|  | 105 | } | 
|---|
|  | 106 | } | 
|---|
|  | 107 |  | 
|---|
|  | 108 | public void SignalSendQueue () { | 
|---|
|  | 109 | evSendRequest.Set (); | 
|---|
|  | 110 | } | 
|---|
|  | 111 | } | 
|---|
|  | 112 | } | 
|---|