source: TFP-WebServer/WebServer/src/UrlHandlers/SseHandler.cs

Last change on this file was 499, checked in by alloc, 4 months ago

*Fixed: Chat code
*Fixed: SSE connection counting, added connection set up logging

File size: 5.2 KB
Line 
1using System;
2using System.Collections.Generic;
3using System.Net;
4using System.Reflection;
5using System.Threading;
6using Webserver.Permissions;
7using Webserver.SSE;
8
9// Implemented following HTML spec
10// https://html.spec.whatwg.org/multipage/server-sent-events.html
11
12namespace Webserver.UrlHandlers {
13 public class SseHandler : AbsHandler {
14 private readonly Dictionary<string, AbsEvent> events = new CaseInsensitiveStringDictionary<AbsEvent> ();
15
16 private ThreadManager.ThreadInfo queueThead;
17 private readonly AutoResetEvent evSendRequest = new AutoResetEvent (false);
18 private bool shutdown;
19
20 private static readonly Type[] ctorTypes = { typeof (SseHandler) };
21 private static readonly object[] ctorParams = new object[1];
22
23 private readonly List<SseClient> clients = new List<SseClient>();
24
25 public SseHandler (string _moduleName = null) : base (_moduleName) {
26 ctorParams[0] = this;
27
28 ReflectionHelpers.FindTypesImplementingBase (typeof (AbsEvent), apiFoundCallback);
29 }
30
31 private void apiFoundCallback (Type _type) {
32 ConstructorInfo ctor = _type.GetConstructor (ctorTypes);
33 if (ctor == null) {
34 return;
35 }
36
37 AbsEvent apiInstance = (AbsEvent)ctor.Invoke (ctorParams);
38 AddEvent (apiInstance.Name, apiInstance);
39 }
40
41 public override void SetBasePathAndParent (Web _parent, string _relativePath) {
42 base.SetBasePathAndParent (_parent, _relativePath);
43
44 queueThead = ThreadManager.StartThread ($"SSE-Processing_{urlBasePath}", QueueProcessThread, ThreadPriority.BelowNormal,
45 _useRealThread: true);
46 }
47
48 public override void Shutdown () {
49 base.Shutdown ();
50 shutdown = true;
51 SignalSendQueue ();
52 }
53
54 // ReSharper disable once MemberCanBePrivate.Global
55 public void AddEvent (string _eventName, AbsEvent _eventInstance) {
56 events.Add (_eventName, _eventInstance);
57 AdminWebModules.Instance.AddKnownModule (new AdminWebModules.WebModule($"webevent.{_eventName}", _eventInstance.DefaultPermissionLevel (), true));
58 }
59
60 public override void HandleRequest (RequestContext _context) {
61 string eventNames = _context.QueryParameters ["events"];
62 if (string.IsNullOrEmpty (eventNames)) {
63 Log.Warning ($"[Web] [SSE] In {nameof (SseHandler)}.HandleRequest(): No 'events' query parameter given");
64 _context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
65 return;
66 }
67
68 SseClient client;
69 try {
70 client = new SseClient(this, _context);
71 } catch (Exception e) {
72 Log.Error ($"[Web] [SSE] In {nameof (SseHandler)}.HandleRequest(): Could not create client:");
73 Log.Exception (e);
74 _context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
75 return;
76 }
77
78 int eventsFound = 0;
79 int eventsAuthorized = 0;
80 int eventsRegistered = 0;
81 foreach (string eventName in eventNames.Split (',', StringSplitOptions.RemoveEmptyEntries)) {
82 if (!events.TryGetValue (eventName, out AbsEvent eventInstance)) {
83 Log.Warning ($"[Web] [SSE] In {nameof (SseHandler)}.HandleRequest(): No handler found for event \"{eventName}\"");
84 continue;
85 }
86
87 eventsFound++;
88
89 if (!IsAuthorizedForEvent (eventName, _context.PermissionLevel)) {
90 continue;
91 }
92
93 eventsAuthorized++;
94
95 try
96 {
97 eventInstance.AddListener (client);
98 } catch (Exception e) {
99 Log.Error ($"[Web] [SSE] In {nameof (SseHandler)}.HandleRequest(): Handler {eventInstance.Name} threw an exception:");
100 Log.Exception (e);
101 _context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
102 }
103
104 eventsRegistered++;
105 }
106
107 if (eventsFound == 0) {
108 _context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
109 _context.Response.Close ();
110 return;
111 }
112
113 if (eventsAuthorized == 0) {
114 _context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
115 if (_context.Connection != null) {
116 //Log.Out ($"{nameof(SseHandler)}: user '{user.SteamID}' not allowed to access '{eventName}'");
117 }
118
119 _context.Response.Close ();
120 return;
121 }
122
123 clients.Add (client);
124 }
125
126 private bool IsAuthorizedForEvent (string _eventName, int _permissionLevel) {
127 return AdminWebModules.Instance.ModuleAllowedWithLevel ($"webevent.{_eventName}", _permissionLevel);
128 }
129
130 private void QueueProcessThread (ThreadManager.ThreadInfo _threadInfo) {
131 while (!shutdown && !_threadInfo.TerminationRequested ()) {
132 evSendRequest.WaitOne (500);
133
134 foreach ((string eventName, AbsEvent eventHandler) in events) {
135 try {
136 eventHandler.ProcessSendQueue ();
137 } catch (Exception e) {
138 Log.Error ($"[Web] [SSE] '{eventName}': Error processing send queue");
139 Log.Exception (e);
140 }
141 }
142
143 for (int index = clients.Count - 1; index >= 0; index--) {
144 clients[index].HandleKeepAlive ();
145 }
146 }
147 }
148
149 public void SignalSendQueue () {
150 evSendRequest.Set ();
151 }
152
153 public void ClientClosed (SseClient _client) {
154 foreach ((string eventName, AbsEvent eventHandler) in events) {
155 try {
156 eventHandler.ClientClosed (_client);
157 } catch (Exception e) {
158 Log.Error($"[Web] [SSE] '{eventName}': Error closing client");
159 Log.Exception(e);
160 }
161 }
162
163 clients.Remove (_client);
164 }
165 }
166}
Note: See TracBrowser for help on using the repository browser.