using System; using System.Net; using System.Collections.Generic; using System.Reflection; using System.Threading; using AllocsFixes.NetConnections.Servers.Web.Handlers; using HttpListenerRequest = SpaceWizards.HttpListener.HttpListenerRequest; using HttpListenerResponse = SpaceWizards.HttpListener.HttpListenerResponse; // Implemented following HTML spec // https://html.spec.whatwg.org/multipage/server-sent-events.html namespace AllocsFixes.NetConnections.Servers.Web.SSE { public class SseHandler : AbsHandler { private readonly Dictionary events = new CaseInsensitiveStringDictionary (); private ThreadManager.ThreadInfo queueThead; private readonly AutoResetEvent evSendRequest = new AutoResetEvent (false); private bool shutdown; public SseHandler (string _moduleName = null) : base (_moduleName) { Type[] ctorTypes = { typeof (SseHandler) }; object[] ctorParams = { this }; foreach (Type t in Assembly.GetExecutingAssembly ().GetTypes ()) { if (!t.IsAbstract && t.IsSubclassOf (typeof (EventBase))) { ConstructorInfo ctor = t.GetConstructor (ctorTypes); if (ctor != null) { EventBase apiInstance = (EventBase)ctor.Invoke (ctorParams); AddEvent (apiInstance.Name, apiInstance); } } } } public override void SetBasePathAndParent (Web _parent, string _relativePath) { base.SetBasePathAndParent (_parent, _relativePath); queueThead = ThreadManager.StartThread ("SSE-Processing_" + urlBasePath, QueueProcessThread, ThreadPriority.BelowNormal, _useRealThread: true); } public override void Shutdown () { base.Shutdown (); shutdown = true; SignalSendQueue (); } public void AddEvent (string _eventName, EventBase _eventInstance) { events.Add (_eventName, _eventInstance); WebPermissions.Instance.AddKnownModule ("webevent." + _eventName, _eventInstance.DefaultPermissionLevel ()); } public override void HandleRequest (string _requestPath, HttpListenerRequest _req, HttpListenerResponse _resp, WebConnection _con, int _permissionLevel) { string eventName = _requestPath.Remove (0, urlBasePath.Length); if (!events.TryGetValue (eventName, out EventBase eventInstance)) { Log.Out ($"Error in {nameof (SseHandler)}.HandleRequest(): No handler found for event \"{eventName}\""); _resp.StatusCode = (int)HttpStatusCode.NotFound; return; } if (!IsAuthorizedForEvent (eventName, _permissionLevel)) { _resp.StatusCode = (int)HttpStatusCode.Forbidden; if (_con != null) { //Log.Out ($"{nameof(SseHandler)}: user '{user.SteamID}' not allowed to access '{eventName}'"); } return; } try { eventInstance.AddListener (_resp); // Keep the request open _resp.SendChunked = true; _resp.AddHeader ("Content-Type", "text/event-stream"); _resp.OutputStream.Flush (); } catch (Exception e) { Log.Error ($"Error in {nameof (SseHandler)}.HandleRequest(): Handler {eventInstance.Name} threw an exception:"); Log.Exception (e); _resp.StatusCode = (int)HttpStatusCode.InternalServerError; } } private bool IsAuthorizedForEvent (string _eventName, int _permissionLevel) { return WebPermissions.Instance.ModuleAllowedWithLevel ("webevent." + _eventName, _permissionLevel); } private void QueueProcessThread (ThreadManager.ThreadInfo _threadInfo) { while (!shutdown && !_threadInfo.TerminationRequested ()) { evSendRequest.WaitOne (500); foreach (KeyValuePair kvp in events) { try { kvp.Value.ProcessSendQueue (); } catch (Exception e) { Log.Error ($"SSE ({kvp.Key}): Error processing send queue"); Log.Exception (e); } } } } public void SignalSendQueue () { evSendRequest.Set (); } } }